Home Design Patterns RAII and Drop Guards

RAII and Drop Guards

Ownership and Lifetimes

Tie release to scope, so it still happens on the error path nobody tested.

Purpose

Acquiring a resource creates an object. Releasing it happens when that object goes out of scope. The programmer never writes the release call, which means the programmer never forgets to write the release call.

The name is Bjarne Stroustrup's, and it is a bad one: Resource Acquisition Is Initialization describes the half of the mechanism that matters least. Destruction is where the value is. Rust's name for the same idea, a guard, is more honest about what the object is for.

Design

A guard type owns or borrows the resource and implements a destructor. In Rust that is the Drop trait, whose drop method runs automatically when the value's scope ends. In C++ it is the destructor. The guard is usually a distinct type from the resource itself, returned by the method that acquires it, which is how the type system enforces that you cannot use the resource without holding a guard.

The critical property is that this survives every exit path. A normal return, an early return, a ? that propagates an error, and a panic that unwinds the stack all run the destructor on the way out.

UML class diagram: Interlock returns an InterlockGuard that implements Drop and re-engages the interlock when it leaves scope.
The guard's only job is to exist for a while and then stop existing. There is no release call anywhere in the client code, which is the point.

Example

An Autobot cell has a motion interlock. While it is engaged, the arm cannot move. Running a cycle means releasing it, doing the work, and re-engaging it afterwards, and the re-engaging is not optional: an arm left live in a cell that a person can walk into is the thing the interlock exists to prevent.

The version of this code I would have written in C# has the release and the re-engage as matched calls with the work in between, and a try/finally around it once somebody noticed that an exception in the middle skipped the re-engage. That works. What it relies on is every future author of every future cycle routine remembering to write the finally, and the failure is silent, and it only manifests on the error path.

struct Interlock {
    cell: u8,
}

struct InterlockGuard<'a> {
    lock: &'a Interlock,
}

impl Interlock {
    fn release(&self) -> InterlockGuard<'_> {
        // energize motion for this cell
        InterlockGuard { lock: self }
    }
}

impl<'a> Drop for InterlockGuard<'a> {
    fn drop(&mut self) {
        // re-engage, however we are leaving
    }
}

fn run_cycle(interlock: &Interlock) -> Result<(), Error> {
    let _guard = interlock.release();

    place_part()?;      // early return here still re-engages
    drill_holes()?;     // and here
    inspect()?;         // and a panic in here does too

    Ok(())
}
There is no re-engage call in run_cycle, and no finally. Adding a fourth step that can fail requires the author to know nothing at all about the interlock.

Seen in the Wild

This is one of the most thoroughly adopted ideas in systems programming. C++ has std::lock_guard, unique_ptr and fstream. Rust has MutexGuard, File, Box, and essentially every type that owns anything. Other languages attack the same problem with scoped syntax rather than object lifetime: Python's with, C#'s using and IDisposable, Java's try-with-resources, and Go's defer are all recognizably the same pattern with the cleanup attached to a block instead of to a value.

Comparisons

  • Proxy is what a guard is, structurally. A MutexGuard is a smart reference that controls access to the thing behind it, which is the textbook definition.
  • Object Pool depends on this to be usable at all. A pool whose objects are returned by an explicit call will leak the first time somebody returns early; a pool that hands out guards will not.
  • Facade handles the ordering half of the same problem. A Facade knows that release must follow acquire; RAII makes the release happen without anyone knowing anything.
  • Arena is the bulk version. RAII releases one resource when one scope ends; an arena releases thousands when one value drops.

Why It Exists

My take

RAII exists because manual release is not a discipline problem that better programmers solve. It is a structural problem. The release call has to appear on every path out of a scope, the number of those paths grows with every early return and every error check, and the paths that get skipped are exactly the ones least likely to be exercised in testing. A rule that must be followed at every exit point will eventually be missed at one of them, and the one it is missed at will be an error path.

What makes it the strongest pattern in this group is that it converts a recurring decision into a one-time one. You get the guard type right once, when you write it, and then correctness is free for every caller forever. Compare that with try/finally, which is correct but has to be re-decided by every author of every function that touches the resource.

I have a specific reason to rate this highly. A missed release in an ordinary application is a leak that somebody notices next Tuesday. In the controller, a lock not released was a stall, and a stall on a production line is people standing around at two in the morning waiting for someone to work out which machine is holding what. I have debugged that. Every hour of it would have been saved by a type that could not be left un-released.

Criticisms

Destructors cannot fail usefully. drop returns nothing and cannot be called with a result to check, so a guard that closes a file has nowhere to report that the final flush failed. The standard advice is to expose an explicit close method for callers who care and treat the destructor as a fallback, which means the pattern has quietly stopped being automatic for exactly the cases where failure matters.

There is a genuine footgun in Rust worth naming, because it bites people repeatedly. Binding a guard to _ rather than to a name drops it immediately, so let _ = mutex.lock(); acquires and releases the lock on the same line and protects nothing at all, while let _guard = mutex.lock(); is correct. The two lines look almost identical and behave completely differently. Similarly, drop order is by reverse declaration order within a scope, which is usually what you want and occasionally is not, and the code gives no hint either way.

Finally, RAII is not a guarantee. Leaking a value is safe in Rust, so std::mem::forget, a reference cycle, or a process that aborts rather than unwinding will all skip the destructor. It is a very good default rather than a proof, and any resource whose release genuinely must happen, such as a physical interlock, needs a hardware watchdog behind it regardless of how good the software is.