Scope and finalizers
A Scope is the lifetime of one or more resources. While a scope is open
you can attach finalizers to it; when the scope is closed, those
finalizers run. A finalizer is just an Effect that performs cleanup. This is
the foundation every other resource-management API in Effect is built on.
The high-level way to register a finalizer is Effect.addFinalizer. It adds the
finalizer to the current scope and gives you the Exit value
describing how the scope closed, so cleanup can react to success vs. failure.
import { Effect, Exit, Console } from "effect"
const program = Effect.gen(function*() { // Register two finalizers. Each receives the `Exit` the scope closed with. yield* Effect.addFinalizer((exit) => Console.log(`finalizer 1 — exit was ${exit._tag}`) ) yield* Effect.addFinalizer((exit) => Console.log(`finalizer 2 — exit was ${exit._tag}`) )
yield* Console.log("doing work...") return "result"})
// `Effect.scoped` opens a scope, runs `program`, then closes the scope —// running the finalizers. It also discharges the `Scope` requirement.Effect.runPromise(Effect.scoped(program))/*Output:doing work...finalizer 2 — exit was Success <-- finalizers run in reverse orderfinalizer 1 — exit was Success*/Two things to internalize from this example:
programhas typeEffect<string, never, Scope>. TheScopein the requirements channel means “I register finalizers and need a scope to run them in.”Effect.scopedprovides that scope and removes the requirement.- Finalizers run in reverse order of registration, like unwinding a stack.
Finalizers run on every outcome
Section titled “Finalizers run on every outcome”The point of a finalizer is that it always runs once registered — on success,
on failure, and on interruption. Here the effect fails, and the finalizer still
fires and can observe the failure through the Exit.
import { Effect, Console } from "effect"
const program = Effect.gen(function*() { yield* Effect.addFinalizer((exit) => Console.log(`cleaning up — exit was ${exit._tag}`) ) // Fail partway through. return yield* Effect.fail("boom")})
Effect.runPromiseExit(Effect.scoped(program)).then(console.log)/*Output:cleaning up — exit was Failure{ _id: 'Exit', _tag: 'Failure', cause: { ... 'boom' ... } }*/ensuring, onExit, and onError
Section titled “ensuring, onExit, and onError”Effect.addFinalizer targets the surrounding scope. When you only want cleanup
attached to a single effect — no scope in the type — reach for these
combinators instead. They mirror try/finally/catch:
Effect.ensuringruns a finalizer whether the effect succeeds, fails, or is interrupted. It does not see the result. This is the closest analogue to afinallyblock.Effect.onExitruns cleanup and receives the fullExit, so it can branch on success vs. failure vs. interruption. The cleanup is uninterruptible.Effect.onErrorruns cleanup only when the effect fails (including by interruption), receiving theCause.
import { Effect, Exit, Cause, Console } from "effect"
const work = Console.log("working...").pipe(Effect.as(42))
const program = work.pipe( // Always runs — good for releasing a lock or logging completion. Effect.ensuring(Console.log("ensuring: always runs")), // Sees the outcome — branch on how the effect ended. Effect.onExit((exit) => Console.log( Exit.isSuccess(exit) ? `onExit: succeeded with ${exit.value}` : "onExit: did not succeed" ) ), // Only runs on failure — receives the Cause for diagnostics. Effect.onError((cause) => Console.log(`onError: ${Cause.pretty(cause)}`)))
Effect.runPromise(program)/*Output:working...onExit: succeeded with 42ensuring: always runs*/Nested lifetimes
Section titled “Nested lifetimes”Scopes compose. Effect.scoped always opens a fresh scope around the effect
it wraps, so you can carve out a shorter lifetime in the middle of a longer one.
Anything acquired inside the inner scope is released as soon as that inner block
finishes, while resources acquired in the outer scope live until the outer block
ends.
import { Effect, Console } from "effect"
const resource = (name: string) => Effect.acquireRelease( Console.log(`acquire ${name}`).pipe(Effect.as(name)), () => Console.log(`release ${name}`) )
const program = Effect.gen(function*() { // `outer` lives for the whole `program`. yield* resource("outer")
// `inner` lives only for this nested scoped block — it is released // immediately when the block completes, before "after inner". yield* Effect.scoped( Effect.gen(function*() { yield* resource("inner") yield* Console.log("using inner") }) )
yield* Console.log("after inner")})
Effect.runPromise(Effect.scoped(program))/*Output:acquire outeracquire innerusing innerrelease inner <-- inner scope closes hereafter innerrelease outer <-- outer scope closes when program ends*/Consuming a scoped resource
Section titled “Consuming a scoped resource”A value of type Effect<A, E, Scope> is scoped: it has acquired something and
registered a finalizer, and it cannot run until something provides a Scope.
Effect.scoped is how you discharge that requirement at the boundary of a
workflow — it opens a fresh scope, runs the effect, and closes the scope (with
the effect’s own Exit) the instant the workflow finishes.
import { Effect, Console } from "effect"
// `db` is scoped: its type is Effect<Db, never, Scope>.const db = Effect.acquireRelease( Console.log("open connection").pipe(Effect.as({ query: () => "rows" })), () => Console.log("close connection"))
const useDb = Effect.gen(function*() { const conn = yield* db yield* Console.log(`query returned: ${conn.query()}`)})// useDb : Effect<void, never, Scope>
// `Effect.scoped` provides the scope and removes it from the requirements,// closing the connection as soon as `useDb` completes.Effect.runPromise(Effect.scoped(useDb))/*Output:open connectionquery returned: rowsclose connection*/When the resource should outlive a single workflow — for example, a connection pool shared by an entire service — provide the scope at the application boundary instead. Layers hold scoped resources for the lifetime of the layer, which is the usual real-world pattern.
Working with a scope directly
Section titled “Working with a scope directly”For most code you never touch a Scope value — Effect.scoped and Layers
manage it. When you do need explicit control (for example, handing the same
scope to several independent tasks), Effect.scopedWith gives you the scope and
lets you register finalizers on it with Scope.addFinalizer.
import { Effect, Scope, Console } from "effect"
const program = Effect.scopedWith((scope) => Effect.gen(function*() { // Attach finalizers to the explicit scope. yield* Scope.addFinalizer(scope, Console.log("release A")) yield* Scope.addFinalizer(scope, Console.log("release B")) yield* Console.log("work") // When `scopedWith` returns, it closes `scope`, running both finalizers // in reverse order: "release B", then "release A". }))
Effect.runPromise(program)/*Output:workrelease Brelease A*/Effect-level reference
Section titled “Effect-level reference”These Effect.* combinators are what application code reaches for daily. They
operate on the current scope (provided by Effect.scoped or a Layer) without
ever forcing you to handle a Scope value by hand.
Effect.addFinalizer
Section titled “Effect.addFinalizer”Registers a finalizer on the current scope. The finalizer receives the Exit
the scope closed with, and always runs once registered.
import { Effect, Exit, Console } from "effect"
const program = Effect.gen(function*() { yield* Effect.addFinalizer((exit) => Console.log(`closing — ${Exit.isSuccess(exit) ? "ok" : "error"}`) ) return "done"})
Effect.runPromise(Effect.scoped(program)).then(console.log)// => closing — ok// => doneEffect.acquireRelease
Section titled “Effect.acquireRelease”Builds a scoped resource from an acquire effect and a release finalizer.
Acquisition is run uninterruptibly by default, and the release runs when the
surrounding scope closes (receiving the Exit). The result requires Scope.
import { Effect, Console } from "effect"
const file = Effect.acquireRelease( Console.log("open file").pipe(Effect.as("handle")), (handle, exit) => Console.log(`close ${handle} (${exit._tag})`))// file : Effect<string, never, Scope>
Effect.runPromise(Effect.scoped(file))// => open file// => close handle (Success)Pass { interruptible: true } as the third argument if acquisition should be
interruptible. See acquire / release
for the full treatment.
Effect.acquireUseRelease
Section titled “Effect.acquireUseRelease”Brackets acquire, use, and release in a single effect — the classic
bracket/try-with-resources shape. Unlike acquireRelease, it does not
add Scope to the requirements: the resource lives exactly for the use
callback.
import { Effect, Console } from "effect"
const program = Effect.acquireUseRelease( // acquire Console.log("connect").pipe(Effect.as({ query: () => "rows" })), // use (db) => Console.log(`got: ${db.query()}`), // release — receives the resource and the Exit of `use` (_db, exit) => Console.log(`disconnect (${exit._tag})`))
Effect.runPromise(program)// => connect// => got: rows// => disconnect (Success)Effect.scoped
Section titled “Effect.scoped”Provides a fresh scope to an effect, runs it, then closes the scope with the
effect’s Exit. This is the standard way to discharge a Scope requirement at
the edge of a workflow.
import { Effect, Console } from "effect"
const scopedWork = Effect.gen(function*() { yield* Effect.addFinalizer(() => Console.log("cleanup")) return 42})// scopedWork : Effect<number, never, Scope>
Effect.runPromise(Effect.scoped(scopedWork)).then(console.log)// => cleanup// => 42Effect.scopedWith
Section titled “Effect.scopedWith”Like Effect.scoped, but passes the freshly created Scope to a callback so
you can register finalizers on it directly. The scope is closed when the
callback’s effect completes.
import { Effect, Scope, Console } from "effect"
const program = Effect.scopedWith((scope) => Scope.addFinalizer(scope, Console.log("released")).pipe( Effect.andThen(Console.log("running")) ))
Effect.runPromise(program)// => running// => releasedEffect.scope
Section titled “Effect.scope”Accesses the current Scope from context. It has type
Effect<Scope, never, Scope> — it both requires and yields the active scope —
so use it inside a scoped workflow when you need the scope value itself.
import { Effect, Scope, Console } from "effect"
const program = Effect.gen(function*() { // Grab the active scope and register a finalizer on it. const scope = yield* Effect.scope yield* Scope.addFinalizer(scope, Console.log("done")) yield* Console.log("work")})
Effect.runPromise(Effect.scoped(program))// => work// => doneEffect.ensuring
Section titled “Effect.ensuring”Attaches a finalizer to a single effect. It runs on success, failure, or
interruption but does not observe the result. No Scope is added to the type.
import { Effect, Console } from "effect"
const program = Console.log("task").pipe( Effect.ensuring(Console.log("always")))
Effect.runPromise(program)// => task// => alwaysEffect.onExit
Section titled “Effect.onExit”Runs cleanup that receives the full Exit of the effect. The cleanup is
uninterruptible, so it is safe for teardown that must complete.
import { Effect, Exit, Console } from "effect"
const program = Effect.succeed(1).pipe( Effect.onExit((exit) => Console.log(`exit: ${exit._tag}`)))
Effect.runPromise(program)// => exit: SuccessEffect.onError
Section titled “Effect.onError”Runs cleanup only when the effect fails (including by interruption), passing the
Cause. On success the cleanup is skipped.
import { Effect, Cause, Console } from "effect"
const program = Effect.fail("boom").pipe( Effect.onError((cause) => Console.log(`failed: ${Cause.pretty(cause)}`)))
Effect.runPromiseExit(program)// => failed: Error: boomScope module reference
Section titled “Scope module reference”The Scope module exposes the primitives the combinators above are built from.
You reach for it when you need explicit control over a lifetime boundary — for
example wiring infrastructure, sharing one scope across several tasks, or
building a resource pool. A Scope has a
strategy ("sequential" finalizers run in reverse registration order;
"parallel" run concurrently) and a state that is Empty, Open, or
Closed.
Scope (interface)
Section titled “Scope (interface)”A Scope is a mutable lifetime boundary: while state._tag === "Open" it
accepts finalizers, and closing it runs them. It exposes strategy and
state.
import { Effect, Scope, Console } from "effect"
const program = Effect.gen(function*() { const scope = yield* Scope.make("sequential") console.log(scope.strategy) // => "sequential" console.log(scope.state._tag) // => "Empty"
yield* Scope.addFinalizer(scope, Console.log("cleanup")) console.log(scope.state._tag) // => "Open"})Closeable (interface)
Section titled “Closeable (interface)”A Closeable extends Scope with the ability to be closed via
Scope.close or Scope.use. The scope constructors
return Closeable scopes; the Scope service tag exposes only the narrower
Scope interface.
import { Effect, Exit, Scope } from "effect"
const program = Effect.gen(function*() { const scope: Scope.Closeable = yield* Scope.make() yield* Scope.close(scope, Exit.void) // only Closeable scopes can be closed})Scope.Scope
Section titled “Scope.Scope”The service tag (and reference) for the active scope in context. Yield it inside
a scoped workflow to obtain the current Scope.
import { Effect, Scope, Console } from "effect"
const program = Effect.gen(function*() { const scope = yield* Scope.Scope yield* Scope.addFinalizer(scope, Console.log("cleanup"))})
// `Effect.scoped` provides the Scope service.Effect.runPromise(Effect.scoped(program))// => cleanupScope.make
Section titled “Scope.make”Creates a fresh Closeable scope as an effect, with an optional finalizer
strategy ("sequential" by default).
import { Effect, Exit, Scope, Console } from "effect"
const program = Effect.gen(function*() { const scope = yield* Scope.make("sequential") yield* Scope.addFinalizer(scope, Console.log("1")) yield* Scope.addFinalizer(scope, Console.log("2")) yield* Scope.close(scope, Exit.void) // sequential => reverse order})
Effect.runPromise(program)// => 2// => 1Scope.makeUnsafe
Section titled “Scope.makeUnsafe”Creates a Closeable scope synchronously, outside the Effect runtime. Use
only when integrating with non-Effect code that needs a scope immediately.
import { Scope } from "effect"
const scope = Scope.makeUnsafe("parallel")console.log(scope.strategy) // => "parallel"console.log(scope.state._tag) // => "Empty"Scope.provide
Section titled “Scope.provide”Provides a concrete Scope to an effect that requires one, removing Scope
from its requirements. Unlike use, it does not close the
scope afterwards — you remain responsible for closing it.
import { Effect, Exit, Scope, Console } from "effect"
const needsScope = Effect.gen(function*() { const scope = yield* Scope.Scope yield* Scope.addFinalizer(scope, Console.log("released")) yield* Console.log("working")})
const program = Effect.gen(function*() { const scope = yield* Scope.make() yield* Scope.provide(needsScope, scope) // requirement discharged, not closed yield* Console.log("before close") yield* Scope.close(scope, Exit.void) // we close it ourselves})
Effect.runPromise(program)// => working// => before close// => releasedScope.addFinalizer
Section titled “Scope.addFinalizer”Registers a finalizer on a scope that ignores the closing Exit. If the scope
is already closed, the finalizer runs immediately.
import { Effect, Exit, Scope, Console } from "effect"
const program = Effect.gen(function*() { const scope = yield* Scope.make() yield* Scope.addFinalizer(scope, Console.log("cleanup")) yield* Scope.close(scope, Exit.void)})
Effect.runPromise(program)// => cleanupScope.addFinalizerExit
Section titled “Scope.addFinalizerExit”Registers a finalizer that receives the Exit the scope was closed with, so it
can branch on success vs. failure. If the scope is already closed, it runs
immediately with the stored Exit.
import { Effect, Exit, Scope, Console } from "effect"
const program = Effect.gen(function*() { const scope = yield* Scope.make() yield* Scope.addFinalizerExit(scope, (exit) => Console.log(`closing with ${exit._tag}`) ) yield* Scope.close(scope, Exit.fail("boom"))})
Effect.runPromise(program)// => closing with FailureScope.fork
Section titled “Scope.fork”Creates a Closeable child scope registered with a parent. Closing the parent
closes the child with the same Exit; closing the child first detaches it from
the parent. The child takes its own finalizer strategy.
import { Effect, Exit, Scope, Console } from "effect"
const program = Effect.gen(function*() { const parent = yield* Scope.make("sequential") yield* Scope.addFinalizer(parent, Console.log("parent cleanup"))
// Child lifetime is bound to the parent. const child = yield* Scope.fork(parent, "parallel") yield* Scope.addFinalizer(child, Console.log("child cleanup"))
// Closing the parent also closes the still-open child. yield* Scope.close(parent, Exit.void)})
Effect.runPromise(program)// => child cleanup// => parent cleanupScope.forkUnsafe
Section titled “Scope.forkUnsafe”The synchronous variant of fork: creates a child scope without
wrapping it in an Effect. Use only in low-level, non-Effect integration code.
import { Scope } from "effect"
const parent = Scope.makeUnsafe("sequential")const child = Scope.forkUnsafe(parent, "parallel")
console.log(child.strategy) // => "parallel"console.log(child.state._tag) // => "Empty"Scope.close
Section titled “Scope.close”Closes a scope and runs its finalizers with the supplied Exit. This is itself
an Effect — finalizers run only when it is executed.
import { Effect, Exit, Scope, Console } from "effect"
const program = Effect.gen(function*() { const scope = yield* Scope.make("sequential") yield* Scope.addFinalizer(scope, Console.log("a")) yield* Scope.addFinalizer(scope, Console.log("b")) yield* Scope.close(scope, Exit.succeed("ok"))})
Effect.runPromise(program)// => b// => aScope.closeUnsafe
Section titled “Scope.closeUnsafe”Transitions a scope to Closed synchronously and returns an Effect that runs
the finalizers (or undefined if there is nothing to run). For low-level
machinery only — ignoring the returned effect skips the finalizers.
import { Effect, Exit, Scope, Console } from "effect"
const scope = Scope.makeUnsafe()Effect.runSync(Scope.addFinalizer(scope, Console.log("cleanup")))
// Returns the finalizer effect; you must run it.const finalize = Scope.closeUnsafe(scope, Exit.void)if (finalize) Effect.runSync(finalize)// => cleanupScope.use
Section titled “Scope.use”Runs an effect with a Closeable scope in its context and closes that scope
with the effect’s Exit when it exits. Use it when you already hold a scope and
want automatic closing (whereas provide leaves it open).
import { Effect, Scope, Console } from "effect"
const work = Effect.gen(function*() { const scope = yield* Scope.Scope yield* Scope.addFinalizer(scope, Console.log("released")) yield* Console.log("working")})
const program = Effect.gen(function*() { const scope = yield* Scope.make() yield* Scope.use(work, scope) // runs `work`, then closes `scope`})
Effect.runPromise(program)// => working// => releasedNow that you understand scopes and finalizers, the next page covers the ergonomic, leak-safe way to define resources: acquire / release. To pool and reuse expensive scoped resources, see Pool.