Skip to main content
Background Image
  1. Blog/

Compile-Time Resource Tracking in Rust: From Runtime Brackets to Type-Level Safety

Author
Glen Baker
Building tech startups and open source tooling
Table of Contents

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 — Returns BracketError with 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:

  • PhantomData for type-level annotations (zero-sized)
  • Associated types for resource set tracking (computed at compile time)
  • The Tracked wrapper 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
#

ApproachLeak DetectionAsync-SafeProtocol EnforcementRuntime Cost
RAII (Drop)RuntimeLimitedNoMinimal
Stillwater bracketRuntimeYesNoMinimal
Stillwater bracket::<R>()Compile timeYesYesZero

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:

  1. Resource leaks are high-severity bugs (connection pools, file systems, critical sections)
  2. Protocols must be followed (begin → work → commit/rollback)
  3. Effects are composed across function boundaries
  4. 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:

  1. Runtime brackets (bracket, bracket2, acquiring) — Guarantee cleanup always runs, even on errors or panics
  2. 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.

Related

Stillwater Validation for Rustaceans: Accumulating Errors Instead of Failing Fast
Refactoring a God Object Detector That Was Itself a God Object
Three Patterns That Made Prodigy's Functional Migration Worth It
God Object Detection Done Right: Why SonarQube's Approach Creates False Positives
Premortem vs Figment: Configuration Libraries for Rust Applications
Mindset - Zero-Cost State Machines for Rust
Postmortem - Schema Validation for Rust
Stillwater - Pure Core, Imperative Shell for Rust