Acquire / release
The acquireRelease family is the recommended way to manage a resource. It
pairs an acquire effect with a release effect and guarantees the release
runs when the scope closes. Crucially, acquisition runs in an uninterruptible
region: once you have started acquiring, you are guaranteed to either fully
acquire the resource (and register its release) or not acquire it at all. There
is no window where a resource is half-open with no finalizer attached — the
classic source of leaks.
Effect.acquireRelease — scoped resources
Section titled “Effect.acquireRelease — scoped resources”Effect.acquireRelease(acquire, release) produces an Effect that yields the
resource and adds release to the current scope. The release receives the
Exit the scope closed with, so it can behave differently on success vs.
failure.
import { Effect, Exit, Console } from "effect"
interface FileHandle { readonly path: string}
// `acquire` opens the resource; `release` closes it and can inspect the Exit.const openFile = (path: string) => Effect.acquireRelease( Console.log(`opening ${path}`).pipe(Effect.as<FileHandle>({ path })), (handle, exit) => Console.log( `closing ${handle.path} (${Exit.isSuccess(exit) ? "ok" : "errored"})` ) )
const program = Effect.gen(function*() { // Yielding the resource registers its release in the surrounding scope. const file = yield* openFile("/tmp/data.txt") yield* Console.log(`reading ${file.path}`) return "contents"})
// `Effect.scoped` defines the lifetime: the file is closed when the block ends.Effect.runPromise(Effect.scoped(program)).then(console.log)/*Output:opening /tmp/data.txtreading /tmp/data.txtclosing /tmp/data.txt (ok)contents*/The resource type carries Scope in its requirements
(Effect<FileHandle, never, Scope>) until you wrap it with Effect.scoped (or
provide it to a Layer, see below). That requirement is the type system enforcing
that the resource must live inside some scope.
Effect.acquireUseRelease — the bracket pattern
Section titled “Effect.acquireUseRelease — the bracket pattern”When a resource is acquired, used, and released all in one place, reach for
Effect.acquireUseRelease(acquire, use, release). It is the classic bracket:
the resource is acquired, the use function runs with it, and release runs
afterward — even if use fails or is interrupted. Unlike acquireRelease, it
does not require a Scope; the lifetime is exactly the use call.
import { Effect, Console } from "effect"
interface Connection { readonly id: number readonly query: (sql: string) => Effect.Effect<ReadonlyArray<string>>}
let nextId = 0
const acquire = Effect.sync<Connection>(() => { const id = ++nextId return { id, query: (sql) => Console.log(`[conn ${id}] ${sql}`).pipe(Effect.as(["row"])) }}).pipe(Effect.tap((c) => Console.log(`acquired connection ${c.id}`)))
const program = Effect.acquireUseRelease( // 1. acquire the resource acquire, // 2. use it — this is the only place the resource is in scope (conn) => conn.query("SELECT 1"), // 3. release it, with access to the resource and the Exit of `use` (conn) => Console.log(`releasing connection ${conn.id}`))
Effect.runPromise(program).then(console.log)/*Output:acquired connection 1[conn 1] SELECT 1releasing connection 1[ 'row' ]*/Which one do I use?
- Use
acquireUseReleasewhen acquire → use → release is a single self-contained operation. It needs no scope and reads top to bottom. - Use
acquireReleasewhen the resource must outlive a single expression — when several steps share it, or when it backs a long-lived service. You decide its lifetime by choosing where to callEffect.scoped, or by attaching it to a Layer.
Tying a resource to a service Layer
Section titled “Tying a resource to a service Layer”The most common production pattern is to acquire a resource while constructing a
service. A Layer built with Layer.effect runs inside
its own scope, so a resource acquired with acquireRelease during construction
is released exactly when the layer is torn down — typically at application
shutdown. You get correct startup/shutdown ordering for free.
import { Config, Context, Effect, Layer, Redacted, Schema } from "effect"import * as NodeMailer from "nodemailer"
// Use `Schema.Defect` to carry an unknown thrown cause on the error.class SmtpError extends Schema.TaggedErrorClass<SmtpError>()("SmtpError", { cause: Schema.Defect}) {}
// A v4 service: `Context.Service` with a static `layer`.class Smtp extends Context.Service<Smtp, { send(message: { readonly to: string readonly subject: string readonly body: string }): Effect.Effect<void, SmtpError>}>()("app/Smtp") { static readonly layer = Layer.effect( Smtp, Effect.gen(function*() { const user = yield* Config.string("SMTP_USER") const pass = yield* Config.redacted("SMTP_PASS")
// The transporter is acquired while the layer is built and closed when // the layer's scope closes — i.e. when the app shuts down. No manual // wiring of `Scope` is needed; `Layer.effect` provides it. const transporter = yield* Effect.acquireRelease( Effect.sync(() => NodeMailer.createTransport({ host: "smtp.example.com", port: 587, secure: false, auth: { user, pass: Redacted.value(pass) } }) ), (transporter) => Effect.sync(() => transporter.close()) )
const send = Effect.fn("Smtp.send")((message: { readonly to: string readonly subject: string readonly body: string }) => Effect.tryPromise({ try: () => transporter.sendMail({ from: "Acme Cloud <cloud@acme.com>", to: message.to, subject: message.subject, text: message.body }), catch: (cause) => new SmtpError({ cause }) }).pipe(Effect.asVoid) )
return Smtp.of({ send }) }) )}Because the cleanup is baked into the layer, every consumer of Smtp benefits
automatically — they never see the transporter, the scope, or the finalizer.
This is the idiomatic place for acquireRelease: at the seam between the outside
world and your service graph.
For resources that run continuously in the background rather than answering calls, see background tasks.