Inside effect blocks, use binds an effect whose requirements include Scope, registering its release into the enclosing block’s scope, including release on interruption. The block defines the scope: a block containing use wraps itself in Effect.scoped, and every registered finalizer runs on success, failure, or interruption, LIFO:
export let report = (userId: string) => effect {
use conn = Db.acquire
let rows = conn.query(sql)!
render(rows)
}export const report = (userId: string) =>
Effect.scoped(
Effect.gen(function* () {
const conn = yield* Db.acquire;
const rows = yield* conn.query(sql);
return render(rows);
}),
);To narrow a resource’s lifetime, use a smaller nested block:
effect {
let rows = effect {
use conn = pool.acquire
conn.query(sql)!
}! // conn released here
transform(rows) // runs after release
}Use resource { } when the caller should own the lifetime. It has the same rules without the automatic scope wrapper: Scope stays in its requirements and acquisitions register into the caller’s scope. In the supported transport, Layer.effect consumes this scoped construction; there is no separate Layer.scoped:
let UsersLive = Layer.effect(Users, resource {
use pool = Pg.makePool(cfg)
makeUsersService(pool)
})Acquisition is always written with use: the block type determines the lifetime. Cleanup without an acquired value uses defer:
let serve = effect {
defer Effect.sync(() => instruction.clear(id))
work()!
}defer accepts an existing infallible Effect<void> cleanup. Lift synchronous work with Effect.sync, explicitly discard any returned value, and handle typed cleanup failures before registering them. Cleanups run LIFO on success, typed failure, defect, and interruption. In effect {} the block owns and closes the scope; in resource {} the caller owns it. Exit-sensitive finalizers remain explicit Effect.addFinalizer((exit) => ...) calls. Binding a Scope-carrying effect with plain ! is an error pointing at use. Finalizers run before catch handlers, so recovery never sees a live resource.