In asynchronous JavaScript, built-in concurrency primitives (Promise.all, Promise.race, Promise.any) lack structural integrity guarantees:
Promise.all([p1, p2, p3]), if request p2 rejects after 10ms, Promise.all immediately rejects. However, p1 and p3 continue executing indefinitely in the background, consuming network sockets, memory, and CPU cycles.Promise.race: When one promise resolves first, losing promises are not cancelled. Their pending callbacks continue executing in the Node.js or browser event loop.AbortController instances, which inevitably leaks listeners in deeply nested call trees.The Effect-TS ecosystem solves concurrent execution challenges through the paradigm of Structured Concurrency.
The foundations of this concurrency model are:
Under Effect's structured concurrency, execution branches out as a strict virtual thread tree (Fiber Tree):
+-------------------------------------------------------------------------+
| PARENT FIBER |
| Effect.all([task1, task2, task3], { concurrency: "unbounded" }) |
+-------------------------------------------------------------------------+
| | |
| fork | fork | fork
v v v
+---------------+ +---------------+ +---------------+
| Child Fiber #1| | Child Fiber #2| | Child Fiber #3|
| (Network I/O) | | (FATAL ERROR: | | (Heavy CPU |
| | | fails in 15ms| | computation) |
+---------------+ +---------------+ +---------------+
| | |
| AUTO-INTERRUPT | ERROR TRIGGERED | AUTO-INTERRUPT
v (Interrupt) v v (Interrupt)
+-----------------------------------------------------------------------------+
| INTERRUPTION SIGNAL PROPAGATION: |
| 1. Failure of Fiber #2 immediately notifies the parent fiber. |
| 2. Parent sends Fiber.interrupt signals to fibers #1 and #3. |
| 3. Fibers #1 and #3 halt immediately, executing their Scope finalizers |
| (closing sockets, releasing buffers). Zero orphaned background tasks! |
+-----------------------------------------------------------------------------+Schedule.exponential.concurrency: 4), where a failure in any required service immediately aborts all pending requests.Effect.race: the fastest response wins and the losing socket connection is instantly interrupted.Effect.fork, join, and interrupt#Any effect can be spawned asynchronously into a dedicated fiber using Effect.fork:
import { Effect, Fiber, Duration } from 'effect';
const backgroundTask = Effect.gen(function* () {
yield* Effect.logInfo('Background fiber started...');
// Simulate long-running task
yield* Effect.sleep(Duration.seconds(10));
yield* Effect.logInfo('Background fiber completed.');
return 100;
});
export const mainProgram = Effect.gen(function* () {
// 1. Fork background task into a dedicated green thread (Fiber)
const fiber = yield* Effect.fork(backgroundTask);
// 2. Main fiber executes in parallel
yield* Effect.sleep(Duration.seconds(1));
yield* Effect.logInfo('Main fiber decides to cancel background worker!');
// 3. Gracefully interrupt the child fiber
yield* Fiber.interrupt(fiber);
yield* Effect.logInfo('Background fiber interrupted cleanly with zero leaks.');
});Schedule#The Schedule module provides mathematical models for backoff strategies:
import { Effect, Schedule, Duration, Data, pipe } from 'effect';
export class TransientNetworkError extends Data.TaggedError('TransientNetworkError')<{
readonly statusCode: number;
}> {}
export class FatalAuthError extends Data.TaggedError('FatalAuthError')<{
readonly reason: string;
}> {}
// Fallible network gateway call
let attempt = 0;
const unstableRequest = Effect.gen(function* () {
attempt++;
yield* Effect.logInfo(`Network request attempt #${attempt}`);
if (attempt < 3) {
return yield* Effect.fail(new TransientNetworkError({ statusCode: 503 }));
}
return { data: 'Gateway successfully responded' };
});
// Configure retry schedule:
// Initial pause 100ms -> exponential growth -> max 5 attempts -> randomized jitter
const retrySchedule = pipe(
Schedule.exponential(Duration.millis(100)),
Schedule.jittered, // Protects against Thundering Herd via +/- 20% delta randomization
Schedule.compose(Schedule.recurs(5))
);
// Apply retry policy STRICTLY to transient errors
export const resilientCall = unstableRequest.pipe(
Effect.retry({
schedule: retrySchedule,
while: (error) => error._tag === 'TransientNetworkError',
})
);Effect.race)#The timeout combinator terminates the active fiber when execution exceeds a duration threshold:
import { Effect, Duration } from 'effect';
const fastPrimaryService = Effect.sleep(Duration.millis(200)).pipe(
Effect.as('PRIMARY_REGION_DATA')
);
const backupService = Effect.sleep(Duration.millis(500)).pipe(
Effect.as('BACKUP_REGION_DATA')
);
// 1. Hedging race: First responder wins, losing fiber is guaranteed to be cancelled
export const hedgedRequest = Effect.race(fastPrimaryService, backupService);
// 2. Strict execution timeout
export const timedOperation = hedgedRequest.pipe(
// If operation exceeds 350ms, it is interrupted with NoSuchElementException
Effect.timeout(Duration.millis(350)),
// Graceful fallback handling
Effect.orElse(() => Effect.succeed('FALLBACK_STALE_CACHE'))
);To process a batch of 1,000 tasks without saturating upstream database connection pools, use the concurrency option:
import { Effect, Duration } from 'effect';
const itemIds = Array.from({ length: 1000 }, (_, i) => `item_${i}`);
const processItem = (id: string) =>
Effect.gen(function* () {
yield* Effect.logInfo(`Processing ${id}`);
yield* Effect.sleep(Duration.millis(50));
return `result_${id}`;
});
// Process 1000 items throttled strictly to 5 concurrent fibers
export const throttledBatch = Effect.all(
itemIds.map((id) => processItem(id)),
{ concurrency: 5 } // Concurrency limit
);Spawn virtual thread and register timeout timer within runtime context.
Service returns 503. Schedule calculates exponential backoff delay with jitter.
Fiber sleeps for the calculated duration and safely re-enters the execution block.
If response arrives in time, return value. Otherwise, forcefully interrupt fiber and close sockets.
Here is an enterprise-grade gateway module wrapping external network services with jittered retries, strict deadlines, and defect isolation:
import { Effect, Schedule, Duration, Data, pipe } from 'effect';
export class TimeoutExceededError extends Data.TaggedError('TimeoutExceededError')<{
readonly serviceName: string;
readonly timeoutMs: number;
}> {}
export class GatewayUnavailableError extends Data.TaggedError('GatewayUnavailableError')<{
readonly serviceName: string;
readonly cause: unknown;
}> {}
export interface GatewayConfig {
readonly serviceName: string;
readonly timeoutMs: number;
readonly maxRetries: number;
readonly initialBackoffMs: number;
}
export function executeWithResilience<A, E>(
action: Effect.Effect<A, E, never>,
config: GatewayConfig
): Effect.Effect<A, E | TimeoutExceededError | GatewayUnavailableError, never> {
// 1. Retry policy with exponential jitter
const retryPolicy = pipe(
Schedule.exponential(Duration.millis(config.initialBackoffMs)),
Schedule.jittered,
Schedule.compose(Schedule.recurs(config.maxRetries))
);
return pipe(
action,
// Apply retry schedule to transient failures
Effect.retry(retryPolicy),
// Bound overall execution duration
Effect.timeout(Duration.millis(config.timeoutMs)),
// Map timeout to typed domain error
Effect.mapError((err) => {
if (err instanceof Error && err.name === 'TimeoutException') {
return new TimeoutExceededError({
serviceName: config.serviceName,
timeoutMs: config.timeoutMs,
});
}
return err;
}),
// Catch catastrophic defects and isolate into controlled domain error
Effect.catchAllDefect((defect) =>
Effect.fail(
new GatewayUnavailableError({
serviceName: config.serviceName,
cause: defect,
})
)
)
);
}Mixing Promise.all inside Effect dismantles structured concurrency. If one promise rejects, remaining promises continue running as orphaned background tasks in Node.js memory.
Solution: Always prefer Effect.all(effects, { concurrency: ... }).
Blindly applying Effect.retry(Schedule.recurs(5)) to unauthorized (401) or validation (400) failures wastes bandwidth and floods upstream servers.
Solution: Guard retries with a predicate: while: (err) => isTransient(err).
Effect fibers are cooperative. A tight synchronous while(true) {} loop monopolizes the JavaScript thread, preventing other fibers from handling cancellation signals.
Solution: Yield periodically to the runtime scheduler using yield* Effect.yieldNow().
Identify transient errors (502, 503, 504, ECONNRESET) versus permanent domain errors (400, 401, 404).
Define Schedule.exponential with jitter and a bounded maximum retry count (3-5).
Wrap external operations with Effect.timeout to avoid socket starvation.
Apply { concurrency: N } limits corresponding to upstream pool capacities.
Verify that simulated network aborts interrupt fibers and invoke finalizers cleanly.