The Problem: Runtime Resource Leaks#
Resource leaks are insidious. A forgotten close(), a missing commit(), an exception path that skips cleanup. Your code compiles. It runs. It even works until the connection pool exhausts, the file handles run out, or a transaction holds a lock forever.
The traditional Rust approach uses RAII: wrap resources in structs that clean up in Drop. This works well for ownership-based patterns, but falls short when:
- Resources are passed through async boundaries
- Effects need to be composed and chained
- You want to express protocols (e.g., begin → query → commit/rollback)
- Clean-up logic itself can fail and needs handling
What if the type system could enforce that every resource acquired is eventually released before you run the code?
The Foundation: Runtime Brackets#
Stillwater has provided runtime resource safety since earlier versions through the bracket pattern. The bracket function guarantees cleanup runs even when errors occur:
use stillwater::effect::prelude::*;
let result = bracket(
open_connection(), // Acquire
|conn| async move { conn.close().await }, // Release (always runs)
|conn| fetch_data(conn), // Use
).run(&env).await;
This solves the “forgotten cleanup” problem at runtime. The release function always runs whether it has success or failure, panic or not. Stillwater provides variants for different needs:
bracket— Basic acquire/use/release with guaranteed cleanupbracket2,bracket3— Multiple resources with LIFO cleanup orderbracket_full— ReturnsBracketErrorwith explicit error info for both use and cleanup failuresacquiring— Fluent builder for composing multiple resources
// Multiple resources with the fluent builder
let result = acquiring(open_conn(), |c| async move { c.close().await })
.and(open_file(path), |f| async move { f.close().await })
.with_flat2(|conn, file| process(conn, file))
.run(&env)
.await;
This works. Cleanup happens. But it’s still runtime verification so you don’t know until the code runs that your brackets are balanced.
Stillwater 0.14.0: Type-Level Resource Tracking#
Stillwater 0.14.0 builds on the runtime bracket foundation with compile-time resource tracking. Now you can make the compiler prove your resources are balanced before the code runs.
use stillwater::effect::resource::*;
use stillwater::pure;
// The TYPE says: this acquires a FileRes
fn open_file(path: &str) -> impl ResourceEffect<
Output = String,
Acquires = Has<FileRes>,
Releases = Empty,
> {
pure::<_, String, ()>(format!("handle:{}", path)).acquires::<FileRes>()
}
// The TYPE says: this releases a FileRes
fn close_file(handle: String) -> impl ResourceEffect<
Output = (),
Acquires = Empty,
Releases = Has<FileRes>,
> {
pure::<_, String, ()>(()).releases::<FileRes>()
}
The ResourceEffect trait extends Effect with two associated types:
Acquires: what resources this effect createsReleases: what resources this effect consumes
This is documentation that the compiler can check.
The Bracket Pattern: Guaranteed Resource Neutrality#
The real power comes from resource_bracket. It enforces that an operation acquires a resource, uses it, and releases it:
fn read_file_safely(path: &str) -> impl ResourceEffect<
Output = String,
Acquires = Empty, // <-- Guaranteed by the type system
Releases = Empty, // <-- No leaks possible
> {
bracket::<FileRes>()
.acquire(open_file(path))
.release(|handle| async move { close_file(handle).run(&()).await })
.use_fn(|handle| read_contents(handle))
}
The bracket::<FileRes>() builder captures the resource type once, then infers everything else from the chained method calls.
The return type says Acquires = Empty, Releases = Empty. This means the function is resource-neutral. If your bracket is wrong and the acquire doesn’t match the release then it won’t compile.
Protocol Enforcement: Database Transactions#
Consider database transactions. A transaction must be opened, used, and then either committed or rolled back. Missing the final step is a bug. Let’s make it a compile error:
fn begin_tx() -> impl ResourceEffect<Acquires = Has<TxRes>> {
pure::<_, String, ()>("tx_12345".to_string()).acquires::<TxRes>()
}
fn commit(tx: String) -> impl ResourceEffect<Releases = Has<TxRes>> {
pure::<_, String, ()>(()).releases::<TxRes>()
}
fn rollback(tx: String) -> impl ResourceEffect<Releases = Has<TxRes>> {
pure::<_, String, ()>(()).releases::<TxRes>()
}
fn execute_query(tx: &str, query: &str) -> impl ResourceEffect<
Acquires = Empty,
Releases = Empty,
> {
// Queries are resource-neutral
pure::<_, String, ()>(vec!["row1".to_string()]).neutral()
}
Now a transaction operation that doesn’t close is a type error:
// This function signature promises resource neutrality
fn transfer_funds() -> impl ResourceEffect<Acquires = Empty, Releases = Empty> {
bracket::<TxRes>()
.acquire(begin_tx())
.release(|tx| async move { commit(tx).run(&()).await })
.use_fn(|tx| {
execute_query(tx, "UPDATE accounts SET balance = balance - 100 WHERE id = 1");
execute_query(tx, "UPDATE accounts SET balance = balance + 100 WHERE id = 2");
pure::<_, String, ()>("transferred".to_string())
})
}
The type signature enforces that transactions are properly closed. If you try to return begin_tx() without a matching release, the code won’t compile.
Tracking Multiple Resources#
Real systems juggle multiple resource types. The tracking composes:
// Acquire both a file and database connection
let effect = pure::<_, String, ()>(42)
.acquires::<FileRes>()
.also_acquires::<DbRes>();
// Release both
let cleanup = pure::<_, String, ()>(())
.releases::<FileRes>()
.also_releases::<DbRes>();
The type system tracks Has<FileRes, Has<DbRes>> as a type-level set. Union operations combine sets from chained effects.
Compile-Time Assertions#
For critical code paths, assert resource neutrality explicitly:
fn safe_operation() -> impl ResourceEffect<Acquires = Empty, Releases = Empty> {
let effect = bracket::<FileRes>()
.acquire(open_file("data.txt"))
.release(|h| async move { close_file(h).run(&()).await })
.use_fn(|h| read_contents(h));
// This is a compile-time check, not a runtime assert
assert_resource_neutral(effect)
}
If effect isn’t actually resource-neutral, this fails at compile time. The assertion costs nothing at runtime since it’s purely type-level.
Custom Resource Kinds#
Define your own resource markers for domain-specific tracking:
struct ConnectionPoolRes;
impl ResourceKind for ConnectionPoolRes {
const NAME: &'static str = "ConnectionPool";
}
fn acquire_connection() -> impl ResourceEffect<Acquires = Has<ConnectionPoolRes>> {
pure::<_, String, ()>("conn_42".to_string()).acquires::<ConnectionPoolRes>()
}
fn release_connection(conn: String) -> impl ResourceEffect<Releases = Has<ConnectionPoolRes>> {
pure::<_, String, ()>(()).releases::<ConnectionPoolRes>()
}
The built-in markers (FileRes, DbRes, LockRes, TxRes, SocketRes) cover common cases, but you’re not limited to them.
Zero Runtime Overhead#
This is the crucial point: all tracking happens at compile time. The implementation uses:
PhantomDatafor type-level annotations (zero-sized)- Associated types for resource set tracking (computed at compile time)
- The
Trackedwrapper delegates directly to the inner effect
pub struct Tracked<Eff, Acq: ResourceSet = Empty, Rel: ResourceSet = Empty> {
inner: Eff,
_phantom: PhantomData<(Acq, Rel)>, // Zero bytes
}
impl<Eff: Effect, Acq: ResourceSet, Rel: ResourceSet> Effect for Tracked<Eff, Acq, Rel> {
async fn run(self, env: &Self::Env) -> Result<Self::Output, Self::Error> {
self.inner.run(env).await // Just delegates
}
}
There are no runtime checks, no allocations, no indirection. The tracking is purely for the type checker.
Comparison: RAII vs Bracket vs Type-Level Tracking#
| Approach | Leak Detection | Async-Safe | Protocol Enforcement | Runtime Cost |
|---|---|---|---|---|
RAII (Drop) | Runtime | Limited | No | Minimal |
Stillwater bracket | Runtime | Yes | No | Minimal |
Stillwater bracket::<R>() | Compile time | Yes | Yes | Zero |
RAII works when you own the resource directly. Stillwater’s runtime bracket() ensures cleanup always runs. This is great for simple acquire/use/release patterns. The type-level bracket::<R>() builder goes further: it makes the protocol—acquire, use, release—visible in the type signature and checked before the code runs.
Use them together: runtime brackets for guaranteed cleanup, type-level tracking for compile-time verification of complex protocols.
When to Use This#
Type-level resource tracking shines when:
- Resource leaks are high-severity bugs (connection pools, file systems, critical sections)
- Protocols must be followed (begin → work → commit/rollback)
- Effects are composed across function boundaries
- You want documentation that can’t lie (types are always current)
For simple, single-owner resources, RAII remains the right choice. For complex effect pipelines where resource safety is critical, type-level tracking catches bugs that runtime checks would miss.
Getting Started#
Add stillwater 0.14.0 to your Cargo.toml:
[dependencies]
stillwater = "0.14"
Import the resource tracking module:
use stillwater::effect::resource::*;
use stillwater::pure;
// Start annotating your effects
fn my_acquire() -> impl ResourceEffect<Acquires = Has<FileRes>> {
pure::<_, String, ()>("handle".to_string()).acquires::<FileRes>()
}
The existing Effect API continues to work unchanged. Resource tracking is purely additive and opt-in.
Summary#
Stillwater’s resource management story now has two complementary layers:
- Runtime brackets (
bracket,bracket2,acquiring) — Guarantee cleanup always runs, even on errors or panics - Compile-time tracking (
bracket::<R>()builder,ResourceEffect) — Prove resource protocols are balanced before the code runs
Together they provide defense in depth:
- Runtime brackets ensure cleanup happens in production
- Type-level tracking catches protocol violations during development
- Ergonomic API via builder pattern (single type parameter)
- Zero runtime overhead via PhantomData
- Composable across effect chains and function boundaries
Resources are too important to leave to runtime chance. Start with brackets for guaranteed cleanup. Add type-level tracking when protocols matter.
Stillwater is a Rust library for validation, effect composition, and functional programming patterns. Version 0.14.0 adds compile-time resource tracking as a type-level layer on top of its existing runtime bracket patterns.