Originally published on Entropic Drift
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 cleanup
bracket2,bracket3— Multiple resources with LIFO cleanup order
bracket_full— ReturnsBracketErrorwith explicit error info for both use and cleanup failures
acquiring— 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 creates
Releases: 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.
Want more content like this? Follow me on Dev.to or subscribe to Entropic Drift for posts on AI-powered development workflows, Rust tooling, and technical debt management.
Check out my open-source projects:
SOCIAL SHARE CARD GENERATOR