Effection Examples

Live operation-first demos for Effection. Each example demonstrates a structured concurrency pattern — click the button to run and observe the timing behavior.


1) Retry with Backoff

Automatically retry failed operations with exponential backoff. Each retry waits longer than the previous one, reducing pressure on struggling services.

What to observe: Watch the elapsed time column — each retry waits twice as long as the previous. Adjust "Fail first N attempts" to see more retries.

function* retryWithBackoff(action, maxAttempts = 5, baseMs = 100, logs = [], startTime = 0) {
  let attempt = 0;
  let delay = baseMs;

  while (true) {
    try {
      const elapsed = Math.round(performance.now() - startTime);
      logs.push({ elapsed, event: `attempt ${attempt + 1}` });
      return yield* call(action);
    } catch (error) {
      attempt += 1;
      if (attempt >= maxAttempts) throw error;
      const elapsed = Math.round(performance.now() - startTime);
      logs.push({ elapsed, event: `failed, waiting ${delay}ms before retry` });
      yield* sleep(delay);
      delay *= 2;
    }
  }
}
Run #

2) Timeout Wrapper

Race an operation against a timer. If the work takes too long, the timeout wins and the operation is cancelled.

What to observe: When work duration > timeout, the operation times out. When work duration < timeout, you see the result. The loser of the race is automatically cancelled.

function* withTimeout(ms, operation, logs, startTime) {
  const timeout = Symbol("timeout");
  const result = yield* race([
    (function* () {
      const value = yield* operation;
      return { type: "value", value };
    })(),
    (function* () {
      yield* sleep(ms);
      const elapsed = Math.round(performance.now() - startTime);
      logs.push({ elapsed, event: `timeout fired at ${ms}ms` });
      return { type: "timeout" };
    })(),
  ]);

  if (result.type === "timeout") {
    throw new Error(`timed out after ${ms}ms`);
  }

  return result.value;
}
Run #

3) Resource Lifecycle (setup/cleanup)

Resources encapsulate setup and cleanup in one scope. The finally block runs when the resource is no longer needed — whether the operation succeeds, fails, or is cancelled.

What to observe: The connection is acquired, used, and released automatically. You don't need to remember to call .close().

function* useMockConnection(state, logs, startTime) {
  return yield* resource(function* (provide) {
    state.acquired += 1;
    const elapsed = Math.round(performance.now() - startTime);
    logs.push({ elapsed, event: "connection acquired" });

    const conn = {
      query(sql) {
        return Promise.resolve([{ sql, ok: true }]);
      },
    };

    try {
      yield* provide(conn);
    } finally {
      state.released += 1;
      const elapsed = Math.round(performance.now() - startTime);
      logs.push({ elapsed, event: "connection released (cleanup)" });
    }
  });
}
Run #

4) Fan-out Work Concurrently

Run multiple operations in parallel and wait for all to complete. Total time equals the slowest task, not the sum of all tasks.

What to observe: The TOTAL time is approximately equal to the longest individual task. Tasks A, B, and C run concurrently, not sequentially.

function* runTask(name, ms, logs, startTime) {
  const startElapsed = Math.round(performance.now() - startTime);
  logs.push({ elapsed: startElapsed, event: `${name} started (${ms}ms)` });
  yield* sleep(ms);
  const endElapsed = Math.round(performance.now() - startTime);
  logs.push({ elapsed: endElapsed, event: `${name} completed` });
  return { name, ms };
}

function* fanOutWork(msA, msB, msC, logs, startTime) {
  return yield* all([
    runTask("A", msA, logs, startTime),
    runTask("B", msB, logs, startTime),
    runTask("C", msC, logs, startTime),
  ]);
}
Run # — max(, , ) = ms expected

5) Cancellation-friendly Loop

Long-running loops can be cancelled cleanly. The finally block runs even when cancelled, ensuring cleanup happens.

What to observe: The heartbeat ticks until auto-stop, then "cleanup: heartbeat halted" appears. The finally block ran because Effection cancelled the loop.

function* heartbeat(intervalMs, logs, startTime) {
  let tick = 0;
  try {
    while (true) {
      tick += 1;
      const elapsed = Math.round(performance.now() - startTime);
      logs.push({ elapsed, event: `tick ${tick}` });
      yield* sleep(intervalMs);
    }
  } finally {
    const elapsed = Math.round(performance.now() - startTime);
    logs.push({ elapsed, event: "cleanup: heartbeat halted" });
  }
}
Run # — expecting ~ ticks before stop

Why Effection?

These patterns are possible with Promises, but Effection makes them composable and predictable:

Pattern Promise approach Effection approach
Retry Manual loop + catch yield* composition
Timeout Promise.race + manual cleanup race([...]) with auto-cancel
Resource try/finally + remember to close resource() guarantees cleanup
Fan-out Promise.all all([...]) with cancellation
Cancel AbortController + signal checking Automatic via structured scope

Key insight: When any operation is cancelled in Effection, all child operations are cancelled too, and all finally blocks run. This is "structured concurrency" — no orphaned operations, no leaked resources.