Scope and Resources

Effect guarantees resource cleanup through scopes. When a scope closes - whether through normal completion, failure, or interruption - all registered finalizers run. This is Effect’s replacement for try-finally and using patterns.

Effect.acquireRelease

The bracket pattern: acquire a resource, register a release action, use the resource:

import { Effect } from "effect"

const openFile = (path: string) =>
  Effect.acquireRelease(
    Effect.gen(function*() {
      yield* Effect.log(`Opening ${path}`)
      return { path, content: "file contents" }
    }),
    (file) => Effect.log(`Closing ${file.path}`)
  )

const program = Effect.scoped(
  Effect.gen(function*() {
    const file = yield* openFile("/etc/config.json")
    yield* Effect.log(`Using ${file.path}`)
    return file.content
  })
)
// Output:
// Opening /etc/config.json
// Using /etc/config.json
// Closing /etc/config.json

The release action runs no matter what happens - success, failure, or interruption.

Effect.scoped

Effect.scoped creates a scope, runs your effect, and closes the scope when done. All resources acquired inside run their finalizers on scope close:

const program = Effect.scoped(
  Effect.gen(function*() {
    const conn = yield* Effect.acquireRelease(
      Effect.log("Connecting to DB"),
      () => Effect.log("Disconnecting from DB")
    )

    const cache = yield* Effect.acquireRelease(
      Effect.log("Initializing cache"),
      () => Effect.log("Destroying cache")
    )

    yield* Effect.log("Doing work with DB and cache")
    // Finalizers run in reverse order: cache destroyed, then DB disconnected
  })
)

Finalizers run in reverse registration order (LIFO), mirroring how you’d clean up in try-finally.

Scope.addFinalizer

For custom cleanup logic that doesn’t fit the acquire-release pattern, use Scope.addFinalizer:

import { Effect, Scope } from "effect"

const program = Effect.scoped(
  Effect.gen(function*() {
    const scope = yield* Scope.scope

    yield* Scope.addFinalizer(scope, Effect.log("Custom cleanup"))

    yield* Effect.log("Working...")
    // "Custom cleanup" runs when scope closes
  })
)

Guaranteed Cleanup on Interruption

Cleanup is guaranteed even when a fiber is interrupted:

const program = Effect.scoped(
  Effect.gen(function*() {
    const resource = yield* Effect.acquireRelease(
      Effect.log("Acquired"),
      () => Effect.log("Released")
    )

    yield* Effect.never // blocks forever
  })
)

Effect.runFork(program).pipe(
  // interrupt after 1 second
)
// Output:
// Acquired
// (1 second later, fiber interrupted)
// Released

Gotcha: If you fork a fiber with Effect.forkDaemon (outside the scope), interruption of the parent won’t trigger the child’s finalizers. Resources acquired in daemon fibers only clean up when the daemon itself completes or is interrupted.

Resources in Layers

Layers that acquire resources use Effect.acquireRelease inside Layer.effect. The resource lives for the runtime’s lifetime:

import { Context, Effect, Layer } from "effect"

class Database extends Context.Service<Database, {
  query: (sql: string) => Effect.Effect<unknown[]>
}>()("myapp/Database") {
  static readonly Live = Layer.effect(Database, Effect.gen(function*() {
    const conn = yield* Effect.acquireRelease(
      Effect.log("Opening DB connection"),
      () => Effect.log("Closing DB connection")
    )
    return Database.of({
      query: (sql: string) => Effect.succeed([])
    })
  }))
}

The DB connection’s finalizer runs when the runtime shuts down.

LayerMap for Dynamic Keyed Resources

LayerMap manages resources keyed by an identifier, such as per-tenant database connections. It builds resources on demand and cleans them up when they’re no longer needed:

import { Effect, LayerMap } from "effect"

const TenantDb = LayerMap.Service("TenantDb")({
  // Builds a new resource for each key
  build: (tenantId: string) =>
    Effect.acquireRelease(
      Effect.gen(function*() {
        yield* Effect.log(`Opening DB for tenant ${tenantId}`)
        return { tenantId, query: () => Effect.succeed([]) }
      }),
      () => Effect.log(`Closing DB for tenant ${tenantId}`)
    )
})

const program = Effect.gen(function*() {
  const dbs = yield* TenantDb
  const db = yield* dbs.get("tenant-123") // builds on first access
  return yield* db.query("SELECT * FROM orders")
})

Tip: Use LayerMap when you need per-key resource pools - database connections per tenant, HTTP clients per API host, or caches per namespace. It handles build-on-demand and cleanup automatically.

See Structured Concurrency for how fibers interact with scopes, and Dependency Injection for the full service pattern.