Resource Management
Long-running programs constantly work with resources that must be released: database connections, file handles, sockets, subscriptions, background fibers. If a resource is acquired but never released — because an error was thrown, the fiber was interrupted, or someone simply forgot — you leak it, and leaks degrade reliability over time.
Effect solves this the same way try/finally does in plain JavaScript, but in
a composable, type-tracked way. The central idea is the Scope: a value
that represents the lifetime of one or more resources. You attach finalizers
to a scope, and when the scope closes, every finalizer runs — in reverse order
of registration — regardless of whether the work succeeded, failed, or was
interrupted.
import { Effect, Console } from "effect"
// `Effect.acquireRelease` pairs an acquisition with a guaranteed release.// The release runs when the surrounding scope closes, no matter how the// program ends.const file = Effect.acquireRelease( Console.log("opening file").pipe(Effect.as({ path: "/tmp/data.txt" })), (handle) => Console.log(`closing ${handle.path}`))
const program = Effect.gen(function*() { const handle = yield* file yield* Console.log(`working with ${handle.path}`)})
// `Effect.scoped` opens a fresh scope, runs the work, then closes the scope —// triggering the release finalizer. This also removes `Scope` from the// requirements, making the effect runnable.Effect.runPromise(Effect.scoped(program))/*Output:opening fileworking with /tmp/data.txtclosing /tmp/data.txt*/Notice that program on its own has type Effect<void, never, Scope> — the
Scope in the requirements channel is the compiler telling you “this effect
acquires resources and needs a scope to release them into.” Wrapping it in
Effect.scoped discharges that requirement.
Where scopes come from
Section titled “Where scopes come from”You rarely build scopes by hand. Most of the time a scope is supplied for you:
Effect.scopedwraps a workflow in a fresh scope that closes when the workflow finishes.- A Layer built with
Layer.effecthas its own scope, so resources acquired while constructing a service live exactly as long as that service — they are released when the layer is torn down. - Forked fibers, streams, and the application runtime all manage scopes under the hood.
In this section
Section titled “In this section”- Scope and finalizers — the
Scopetype,Effect.addFinalizer,Effect.ensuring,onExit/onError, and how lifetimes nest. - Acquire / release —
Effect.acquireReleasefor scoped resources andEffect.acquireUseReleasefor the classic bracket pattern, plus tying resources to service layers. - Background tasks —
Layer.effectDiscardandEffect.forkScopedfor fire-and-forget work whose lifetime is bound to a scope, with no service interface to expose.