Resource — Safe Lifecycle Management
A very common sequence in software development is the acquire-use-release lifecycle. You open a database connection, run a series of queries, and close the connection. Or you open a file handle, read its contents, and close the handle when done.
This sequence is simple, until an error occurs. If a query throws a runtime exception, the close instruction is skipped, causing a connection leak that will eventually crash the server.
To fix this, we typically introduce defensive blocks:
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 TaskResult) 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 TaskResult:
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.
When to use Resource
Section titled “When to use Resource”Use Resource when:
Section titled “Use Resource when:”- Managing active assets: Opening and closing file descriptors, network sockets, or database pools.
- Acquiring locks: Coordinating concurrency where a critical section must acquire and release a mutex.
- Tying lifetimes: Starting and stopping background worker threads or child processes associated with a request’s lifetime.
- Composing cleanups: You want the assurance that multiple async resources are safely torn down in reverse order without writing nested, complex error-handling blocks.
Keep using try/finally when:
Section titled “Keep using try/finally when:”- The scope is strictly local and synchronous: Inside a simple, short function where a synchronous resource is opened and immediately closed on the next line, and never leaves the function body.