Skip to content

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 file
working with /tmp/data.txt
closing /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.

You rarely build scopes by hand. Most of the time a scope is supplied for you:

  • Effect.scoped wraps a workflow in a fresh scope that closes when the workflow finishes.
  • A Layer built with Layer.effect has 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.
  • Scope and finalizers — the Scope type, Effect.addFinalizer, Effect.ensuring, onExit/onError, and how lifetimes nest.
  • Acquire / releaseEffect.acquireRelease for scoped resources and Effect.acquireUseRelease for the classic bracket pattern, plus tying resources to service layers.
  • Background tasksLayer.effectDiscard and Effect.forkScoped for fire-and-forget work whose lifetime is bound to a scope, with no service interface to expose.