async-scope-js
v0.1.1
Published
Structured concurrency for JavaScript with scoped async tasks and cancellation.
Readme
async-scope
Structured concurrency for JavaScript & TypeScript async-scope brings Go-style errgroup semantics and structured concurrency to JavaScript.
It solves a fundamental problem in async JS:
Promises make failures contagious. Structured concurrency makes failures controlled.
The Problem
In JavaScript today:
Promise.all([fetchUser(), fetchBilling(), fetchPermissions()]);If any promise fails:
- Everything rejects
- Partial work is lost
- Cancellation is manual
- Errors leak across layers
This creates:
- Cascading failures
- Resource leaks
- Race conditions
- Complex error handling
JavaScript has no built-in concept of a scope for async work.
The Solution: Async Scopes
async-scope introduces structured concurrency:
- All async work runs inside a scope
- ancellation propagates downward
- Errors are collected, not leaked
- Only the scope decides success or failure
- This is how Go, Rust, Kotlin, and Swift handle concurrency.
Now JavaScript can too.
Basic Usage
import { withScope } from "async-scope";
await withScope(async (scope) => {
scope.spawn(async () => {
await fetchUser();
});
scope.spawn(async () => {
await fetchBilling();
});
scope.spawn(async () => {
await fetchPermissions();
});
});If any task fails:
- All siblings are cancelled
- The scope fails
- No leaks, no partial state
Policies
Scopes support two failure policies:
Policy Behavior
- cancel (default) First failure cancels all tasks
- supervise Tasks may fail without cancelling siblings
await withScope(async (scope) => {
scope.spawn(async () => {
throw new Error("non-fatal");
});
scope.spawn(async () => {
await doImportantWork();
});
}, { policy: "supervise" });Optional Tasks
For work that should never affect the scope:
const metrics = await scope.spawnOptional(async () => {
// metrics is undefined if it fails
return await fetchMetrics();
});
Optional tasks:
- Never cancel
- Never fail the scope
- Are still tracked and cancelled when needed
Critical Tasks
For work that must always stop everything on failure:
scope.spawnCritical(async () => {
await commitTransaction();
});Critical tasks:
- Cancel the scope immediately
- Override supervise mode
Nested Scopes
Scopes can be nested safely.
scope.spawn(async () => {
await withScope(async (child) => {
child.spawn(async () => {
await doChildWork();
});
});
});Rules:
- Parent cancellation → cancels child
- Child failure → does not cancel parent
- Errors surface only when awaited
Cancellation
scope.cancel();Cancels:
- All tasks
- All nested scopes
- All future spawns
Tasks receive an AbortSignal:
scope.spawn(async (signal) => {
while (!signal.aborted) {
await doWork();
}
});Business Rules
Scenario | Result spawn() | fails, policy = cancel Cancel all siblings spawn() | fails, policy = supervise Continue spawnCritical() | fails Always cancel spawnOptional() | fails Ignored Child scope fails | Parent unaffected Parent cancelled | Child cancelled User function throws | Scope fails waitForCompletion() | called Errors are thrown
Error Handling
All scope-related failures use ScopeError.
import { ScopeError } from "async-scope";
try {
await withScope(async (scope) => {
scope.spawn(async () => {
throw new Error("boom");
});
});
} catch (err) {
if (err instanceof ScopeError) {
// structured cancellation or task failure
console.log(err.reason);
} else {
throw err;
}
}Inspecting Partial Failures
In supervise mode you can inspect what failed:
await withScope(async (scope) => {
scope.spawn(async () => {
throw new Error("A");
});
scope.spawn(async () => {
throw new Error("B");
});
scope.spawn(async () => {
await importantWork();
});
await scope.waitForCompletion();
console.log(scope.getErrors()); // [Error("A"), Error("B")]
}, { policy: "supervise" });Why this is safer than Promises
Promises async-scope Errors leak Errors contained No cancellation Structured cancellation Partial state All-or-nothing Race conditions Deterministic Hard to reason Hierarchical
Why This Matters
This is the same model used by:
- Go (errgroup)
- Kotlin (coroutineScope)
- Swift (TaskGroup)
- Rust (tokio::scope)
Now JavaScript has it too.
Summary
async-scope gives you:
- Structured concurrency
- Predictable cancellation
- Contained failures
- Safe nesting
- Production-grade async control
