Structured concurrency for JavaScript with scoped async tasks and cancellation.
npm install async-scope-jsjs
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
`JS
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
`JS
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:
`JS
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:
`JS
scope.spawnCritical(async () => {
await commitTransaction();
});
`
Critical tasks:
- Cancel the scope immediately
- Override supervise mode
Nested Scopes
Scopes can be nested safely.
`JS
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
`JS
scope.cancel();
`
Cancels:
- All tasks
- All nested scopes
- All future spawns
Tasks receive an AbortSignal:
`JS
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.
`JS
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:
`JS
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" });
``