Telex logo Telex

Rust Patterns That Matter #9: Drop and RAII

Post 9 of 22 in Rust Patterns That Matter. Companion series: Building a Chat Server in Rust.

Previous: #8: Index-Based Design | Next: #10: Lifetime Annotations

A client disconnects mid-session. They hold a slot in a room's member list, a TCP stream, and a write buffer. If you forget to clean up any of these, you leak resources. In Python you'd use try/finally. In Go, defer. In Rust, the answer is built into the type system.

The motivation

Imagine a chat server where each connection holds several resources:

struct Connection {
    stream: TcpStream,
    room_id: RoomId,
    user_id: UserId,
    buffer: Vec<u8>,
}

When a client disconnects, you need to remove them from the room, close the stream, and free the buffer. The obvious approach is manual cleanup:

fn handle_client(conn: Connection, rooms: &mut RoomRegistry) {
    match conn.run() {
        Ok(()) => {
            rooms.remove_user(conn.room_id, conn.user_id);
        }
        Err(e) => {
            eprintln!("error: {e}");
            rooms.remove_user(conn.room_id, conn.user_id); // again
        }
    }
}

Every exit path needs the same cleanup. Miss one and you leak. Add an early return or a ? and you miss it. This doesn't scale.

RAII - the concept

Resource Acquisition Is Initialization. The idea: acquire the resource when you create the value. Release it when the value is destroyed. In Rust, destruction is deterministic - a value is dropped when it goes out of scope, on every path: normal returns, early returns, ? propagation, and panics (unless the process aborts).

You don't call a cleanup function. You don't register a finalizer. The compiler inserts the cleanup at every scope exit automatically.

The Drop trait

Implement Drop to define what happens when your value is destroyed:

struct RoomGuard {
    room_id: RoomId,
    user_id: UserId,
    rooms: Rc<RefCell<RoomRegistry>>,
}

impl Drop for RoomGuard {
    fn drop(&mut self) {
        self.rooms.borrow_mut().remove_user(self.room_id, self.user_id);
    }
}

Now the handler doesn't need manual cleanup at all:

fn handle_client(stream: TcpStream, rooms: Rc<RefCell<RoomRegistry>>) {
    let user_id = rooms.borrow_mut().add_user();
    let room_id = RoomId(0);
    let _guard = RoomGuard { room_id, user_id, rooms: rooms.clone() };

    // Do work here. If anything panics or returns early,
    // _guard is dropped and the user is removed from the room.
}

The underscore prefix in _guard signals intent: we don't use the variable, we hold it for its destructor. When the function exits - for any reason - _guard is dropped and cleanup runs.

When drop runs

The compiler inserts drop calls at predictable points:

Drop order within a scope is reverse declaration order - the last variable declared is dropped first. This is deliberate: later variables may reference earlier ones, so they must be cleaned up first.

What you can't do in drop

The drop method takes &mut self and returns nothing. This means:

The guard pattern

You've already seen guards in the standard library:

The pattern is always the same: the guard represents a held resource. Creating it acquires the resource. Dropping it releases it. No manual cleanup, no forgotten releases.

Design your own types this way. If a value represents an acquired resource - a database transaction, a temporary file, a registered callback - implement Drop so that cleanup is automatic.

When to use it

What comes next

Drop guarantees cleanup for owned values. But what about data that's borrowed? When a struct holds a reference, the compiler needs to know how long that reference is valid. That's where lifetime annotations come in - the next post.

See it in practice: Building a Chat Server #2: Rooms and Users uses this pattern for automatic connection cleanup.