Show HN: Pure Effect – Reproduce production bugs on your laptop without a DB
Show HN: Pure Effect – Debug Production Bugs Locally (No DB Required)
Pure Effect (v0.8.0) is a lightweight, zero-dependency effect library designed for JavaScript and TypeScript. It allows you to treat your business logic as plain data, enabling you to reproduce production failures on your laptop without needing a live database.
🚀 Quick Stats
- Size: (minified + gzipped)
- Dependencies:
0 - Core Primitives:
6
01 / The Problem: "It works on my machine"
Most bugs are hard to reproduce because business logic and I/O are tightly coupled.
"The cause is usually the same: business logic and I/O are tangled together."
When you use await db.findUser(email), the request is dispatched immediately. To test this, you are forced to use mocks, fakes, or heavy containers. When a request fails in production, a stack trace tells you where it crashed, but it doesn't capture the exact sequence of I/O results that led to the crash.
Comparison: Execution Models
The async / await approach: I/O is the logic.
async function registerUser(input) {
const found = await db.findUser(input.email); // Fires immediately!
if (found) throw new Error('Email in use');
return db.saveUser(input);
}
The pure-effect approach: Logic returns I/O as data.
const flow = registerUserFlow(input);
// We can inspect the "intent" before executing it
assert.equal(flow.cmd.name, 'cmdFindUser');
// We can simulate production results without a DB
const next = flow.next(recordedUser);
assert.equal(next.cmd.name, 'cmdSaveUser');
02 / Core Architecture
Pure Effect transforms your logic into a tree that an interpreter walks at the system's edge.
The Primitives
| Primitive | Role | Description |
|---|---|---|
Failure(error) | ERR | Immediately halts the pipeline and short-circuits. |
Command(cmd, next) | Deferred I/O | A description of a side effect. cmd is the action; next processes the result. |
Ask(next) | Context/DI | Accesses the environment (e.g., tenantId, config) without prop-drilling. |
Retry(effect, opts) | Resilience | Wraps an effect to attempt recovery on failure. |
Parallel(effects, next) | Concurrency | Executes multiple effect trees via Promise.all. |
The Composers
effectPipe(...fns): Chains functions sequentially.runEffect(effect, ctx?): The interpreter that actually executes the I/O.configureEffect(opts): Sets up global hooks.
Visualizing the Flow
03 / Complete Implementation Example
Here is how a user registration flow looks when decoupled from the database:
import { Success, Failure, Command, effectPipe, runEffect } from 'pure-effect';
// 1. Pure Logic: Instantly testable, no I/O
const validateRegistration = (input) => {
if (!input.email.includes('@')) return Failure('Invalid email.');
if (input.password?.length < 8) return Failure('Password too short.');
return Success(input);
};
const ensureEmailAvailable = (found) =>
found ? Failure('Email already in use.') : Success(true);
// 2. Commands: Side effects described as data
const findUserByEmail = (email) => {
const cmdFindUser = () => db.findUserByEmail(email);
return Command(cmdFindUser, (found) => Success(found));
};
const saveUser = (input) => {
const cmdSaveUser = () => db.saveUser(input);
return Command(cmdSaveUser, (saved) => Success(saved));
};
// 3. Pipeline: Composing the flow
const registerUserFlow = (input) => effectPipe(
validateRegistration,
() => findUserByEmail(input.email),
ensureEmailAvailable,
() => saveUser(input)
)(input);
// 4. Testing: Assert on the structure, not the result
const flow = registerUserFlow({ email: 'test@example.com', password: 'password123' });
assert.equal(flow.type, 'Command'); // No DB was touched!
// 5. Execution: Run at the boundary
const saved = await runEffect(registerUserFlow(input), { flowName: 'register' });
04 / Key Features
- Tree Walking: Inspect every step of the logic without mocks or in-memory fakes.
- Production Replay: Record I/O results in production and feed them into the local tree to retrace a failure path.
- Data-Driven Retry: Test resilience without
setTimeout.- Math: If is delay and is backoff, the sequence is
Retry(effect, { attempts: 3, delay: 200, backoff: 2 });
- Concurrent Execution: Run branches in parallel with ordered results.
Parallel([getUser(id), getPerms(id)], ([user, perms]) => Success({ user, perms }));
- Clean DI: Use
Askto resolve tenant or trace IDs from the framework layer. - Observability: Built-in hooks (
onRun,onStep,onBeforeCommand) for OpenTelemetry spans.
05 / Positioning & Comparison
Pure Effect is designed for finite, request-shaped operations. It is not intended for long-running background processes.
| Library | Description | Choose pure-effect if... |
|---|---|---|
| Effect-TS | A massive functional ecosystem (fibers, streaming, schema). | You want testable pipelines and DI that can be learned in one afternoon. |
| fp-ts | Category-theory abstractions (Monads, Applicatives). | You find that mocks drift from real drivers and tests feel like "theater." |
| async/await | Standard JS imperative concurrency. | You need to see exactly what your code would do before it does it. |