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

Published: (December 20, 2025 at 05:50 PM EST)
7 min read
Source: Dev.to

Source: Dev.to
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 the early versions through the bracket pattern.
The bracket function guarantees that 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;

The release function always runs, regardless of whether the use phase succeeds, fails, or panics.

Bracket Variants

VariantDescription
bracketBasic acquire → use → release with guaranteed cleanup
bracket2, bracket3Manage multiple resources; cleanup follows LIFO order
bracket_fullReturns a BracketError that contains explicit error information for both the use and cleanup phases
acquiringFluent builder for composing several resources together

Fluent Builder Example

// 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;

The code works, and cleanup happens automatically.
However, because this safety is enforced at runtime, you won’t know until the program executes that your brackets are correctly balanced.

Stillwater 0.14.0 – Type‑Level Resource Tracking

Stillwater 0.14.0 builds on the runtime bracket foundation with compile‑time resource tracking. The compiler can now prove that 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<Acquires = FileRes, Releases = Empty> {
    pure::(format!("handle:{}", path)).acquires::()
}

// The TYPE says: this releases a `FileRes`
fn close_file(handle: String) -> impl ResourceEffect {
    pure::(()).releases::()
}

The ResourceEffect trait extends Effect with two associated types:

  • Acquires – the resources this effect creates.
  • Releases – the resources this effect consumes.

These associated types serve as documentation that the compiler can verify.

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 {
    bracket::()
        .acquire(open_file(path))
        .release(|handle| async move { close_file(handle).run(&()).await })
        .use_fn(|handle| read_contents(handle))
}
  • bracket::() captures the resource type once, then infers everything else from the chained method calls.
  • The return type shows Acquires = Empty, Releases = Empty, meaning the function is resource‑neutral.
  • If the bracket is mismatched (e.g., the acquire does not match the release), the code will fail to 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‑time error:

fn begin_tx() -> impl ResourceEffect {
    pure::("tx_12345".to_string()).acquires::()
}

fn commit(tx: String) -> impl ResourceEffect {
    pure::(()).releases::()
}

fn rollback(tx: String) -> impl ResourceEffect {
    pure::(()).releases::()
}

fn execute_query(tx: &str, query: &str) -> impl ResourceEffect {
    // Queries are resource‑neutral
    pure::(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 {
    bracket::()
        .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::("transferred".to_string())
        })
}

Enforcing Proper Transaction Closure

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 a database connection
let effect = pure::(42)
    .acquires::()
    .also_acquires::();

// Release both
let cleanup = pure::(())
    .releases::()
    .also_releases::();

The type system tracks Has<…> 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 {
    let effect = bracket::()
        .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)
}

Note: If effect isn’t actually resource‑neutral, this fails at compile time.
The assertion incurs no runtime cost because it operates purely at the 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 {
    pure::("conn_42".to_string()).acquires()
}

fn release_connection(conn: String) -> impl ResourceEffect {
    pure::(()).releases()
}

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:

use std::marker::PhantomData;

pub struct Tracked<Eff> {
    inner: Eff,
    _phantom: PhantomData<()>, // Zero bytes
}

impl<Eff> Effect for Tracked<Eff>
where
    Eff: Effect,
{
    type Env = Eff::Env;
    type Output = Eff::Output;
    type Error = Eff::Error;

    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, and 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::()Compile‑timeYesYesZero
  • RAII works when you own the resource directly.
  • bracket() (runtime) guarantees that cleanup code always runs—ideal for simple acquire/use/release patterns.
  • bracket::() (type‑level) goes further: the acquire‑use‑release protocol is encoded in the type signature and verified at compile time.

How to Use Them Together

  • Use runtime brackets for guaranteed cleanup in asynchronous contexts.
  • Add type‑level tracking to get compile‑time verification of more complex protocols.

This combination gives you both safety (no leaks) and confidence (protocol correctness).

When to Use This

Type‑level resource tracking shines when:

  • Resource leaks are high‑severity bugs (e.g., connection pools, file systems, critical sections)
  • Protocols must be followed (e.g., 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 {
    pure::("handle".to_string()).acquires()
}

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:

LayerPurpose
Runtime brackets (bracket, bracket2, acquiring)Guarantee cleanup always runs, even on errors or panics
Compile‑time tracking (bracket::() 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 a 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, then add type‑level tracking when protocols matter.

About Stillwater

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.

Follow & Explore

Want more content like this?

  • Follow me on Dev.to
  • Subscribe to Entropic Drift for posts on AI‑powered development workflows, Rust tooling, and technical‑debt management.

Open‑source projects:

  • Debtmap – Technical‑debt analyzer
  • Prodigy – AI workflow orchestration
Back to Blog

Related posts

Read more »

Rust got 'pub' wrong

!Cover image for Rust got 'pub' wronghttps://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s...