Scope
Own finalizers with hierarchical, child-first cleanup.
Scope is the lifecycle primitive. A Scope can register finalizers, acquire
resources, add existing disposables and fork children. Only an owner can close
it.
The contextual Scope
Inside a Runtime execution, yield* Scope exposes the non-owning capability:
const program = Effect.gen(async function* () {
const scope = yield* Scope
scope.addFinalizer(async (outcome) => {
console.log('execution ended with', outcome.status)
})
return Result.ok()
})The contextual Scope interface intentionally has no close() method. Code
inside a program can register cleanup and create children without accidentally
closing the Runtime's owner-managed Scope.
Explicit ownership with Scope.make and Scope.run
Use Scope.make() when the caller owns a closeable Scope:
const scope = Scope.make()
try {
const resource = await scope.acquire(
() => connect(),
(connection, outcome) => connection.close(outcome)
)
await resource.query()
} finally {
await scope.close({ status: 'success' })
}Scope.run creates and owns the Scope for you:
await Scope.run(async (scope) => {
const connection = await scope.acquire(connect, (value) => value.close())
return connection.query()
})Scope itself is independent of better-result: even a returned Result.err
is a successful Scope outcome. Result-aware classification belongs to
Runtime.run.
Register finalizers
Finalizers receive the final ScopeOutcome:
scope.addFinalizer(async (outcome) => {
if (outcome.status === 'failure') {
await metrics.increment('failed-execution')
}
})The first close(outcome?) call fixes the outcome. Later calls return the same
Promise. A parent closing propagates its outcome to still-attached children.
Hierarchy and order
fork() creates a child owned by the parent until it closes:
const batch = scope.fork()
await Scope.provide(batch, async () => {
// resources registered here belong to batch
})
await batch.close()Parent closure is child-first and LIFO:
parent closes
├── newest child closes
│ └── child finalizers, newest first
├── older child closes
└── parent finalizers, newest firstClosing continues after one finalizer fails. Failures are aggregated in
ScopeCloseError.causes; nested ScopeCloseError values are flattened when a
parent closes.
Resource acquisition
scope.acquire closes a race where acquisition completes as closure begins. If
registration fails after acquisition, the release callback is attempted
immediately; if both registration and immediate release fail, both causes are
preserved in an AggregateError.
scope.add validates the disposal protocol at runtime and registers the exact
object. It prefers Symbol.asyncDispose, then Symbol.dispose.
Scope errors
ScopeRuntimeNotConfiguredError: no current Scope exists.ScopeClosedError: a resource, child or finalizer was added after closure started.ResourceNotDisposableError: a value has neither disposal protocol.ScopeCloseError: one or more finalizers failed.
Direct Scope.close() does not invoke cleanup observers. Observers belong to
execution and Runtime boundaries, where a primary program result is available.