Resource — Safe Lifecycle Management
Managing resources like database connections, file handles, or mutex locks requires guaranteeing
that cleanup logic runs even if operations throw runtime errors. When coordinating multiple
resources, nested try/finally blocks quickly accumulate boilerplate.
Resource<E, A> encapsulates the acquire-use-release lifecycle (the bracket pattern) into a
composable data structure, guaranteeing that release functions execute whenever an acquired resource
leaves scope.
This works for a single resource within a narrow scope. But as applications grow, we find ourselves
composing multiple resources — like a database connection and a cache socket. Managing nested
try/finally blocks is extremely fragile, and passing a resource across multiple function
boundaries makes it very easy to forget who holds the responsibility to clean it up.
Resource<E, A> solves this structurally by implementing the bracket pattern. It packages the
potentially fallible acquire step (a Task.Result) and the infallible release step (a Task)
into a single, cohesive data structure.
flowchart TD
Start([Resource.use]) --> Acquire[Run Acquire Task]
Acquire -->|Success| Work[Run Work Function]
Acquire -->|Failure| FailExit([Return Acquire Error])
Work -->|Succeeds or Fails| Release[Run Release Task]
Release --> WorkExit([Return Work Result])
By describing how to open and how to close a resource once, we delegate the execution safety to
the library. Resource.use guarantees that the cleanup step is always executed, whether the work
succeeds or fails.
Creating a Resource
Section titled “Creating a Resource”We define a resource by supplying the actions to open and close it.
The release callback receives the exact value produced by the acquisition step. Even if a query
fails midway, the manager will execute connection.close() automatically.
Resources that cannot fail
Section titled “Resources that cannot fail”If acquiring the resource is guaranteed to succeed — such as acquiring an in-memory lock or starting
a local timer — we can use fromTask to skip error mapping:
The error parameter is typed as never to formally declare to the compiler that this resource is
structurally incapable of failing to acquire.
Running Actions with use
Section titled “Running Actions with use”To perform work with our resource, we pass our operational logic to Resource.use. The work
function receives the acquired value and must return a Task.Result:
Let’s trace the execution:
dbResourceexecutes itsacquiretask. If this fails, the execution stops and yields the acquisition error.- If successful, the active connection is passed to the query function.
- Whether the query succeeds or fails, the
releasetask (connection.close()) is immediately executed. - The final result of the query (an
OkorErr) is returned.
Composing Multiple Resources
Section titled “Composing Multiple Resources”When an operation requires multiple distinct resources to execute — for instance, a database connection and a Redis cache client — we can combine them into a single, unified resource.
Parallel acquisition with combine
Section titled “Parallel acquisition with combine”Resource.combine aggregates two resources, presenting them as a single resource carrying a tuple
of both values:
When combining resources, the release tasks are executed in reverse acquisition order: the cache is released first, and the database is released second.
If the database is successfully opened but the cache client fails to acquire, the manager immediately releases the database connection and returns the cache acquisition error. Your system is guaranteed never to leak a connection mid-setup.
Sequential nesting
Section titled “Sequential nesting”For complex workflows where a second resource depends directly on the value of the first (e.g.
initiating a transaction on an active connection), you can nest Resource.use calls:
The transaction resource will release (commit or roll back) before the database connection is closed.
Problems it solves
Section titled “Problems it solves”- Database transaction and connection lifecycle safety: Acquiring database connections and
running transactional mutations requires strict cleanup: committing on success and rolling back on
failure.
Resourceguarantees that release callbacks always execute, preventing orphaned connections or locked tables even when errors occur during execution. - Temporary files, socket descriptors, and child processes: Batch workers and CLI tools often
create scratch directories, open streaming file descriptors, or spawn background child processes.
Resource.makebinds allocation with automated cleanup, ensuring external OS assets are disposed of as soon as execution leaves scope. - Distributed mutex locks and lease coordination: In concurrent worker clusters where jobs
require exclusive access to shared resources, acquiring a distributed lock (e.g. via Redis) must
be paired with guaranteed lock release upon completion or unexpected failure.
Resourceencapsulates lease acquisition and release into a clean, safe bracket. - Composing interdependent resources in reverse teardown order: When a workflow requires
acquiring resource A (a connection pool), resource B (a distributed lock), and resource C (a
temporary staging file), teardown must occur in exact reverse order (C, then B, then A).
Resourcemanages nested resource composition and enforces reverse-order cleanup automatically.