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:
- End of scope: a local variable goes out of scope at the closing brace
- Explicit drop:
drop(value)drops it immediately (this is justfn drop<T>(_: T) {}- it takes ownership and the value is dropped when the function's parameter goes out of scope) - Reassignment:
x = new_valuedrops the old value before assigning - Collection clearing:
vec.clear()drops each element - Option replacement: going from
Some(v)toNonedropsv
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:
- No error returns. If cleanup can fail (e.g., flushing a buffer
to disk), you must handle the error inside
drop- log it, ignore it, or panic. For important fallible cleanup, provide an explicitclose()method and useDropas a safety net. - No moving out of
self. You have&mut self, notself. You can't take ownership of fields. You can useOption::take()if you need to move a field out during drop. - Fields drop automatically after your
drop. You don't need to drop fields manually - the compiler does it after yourdropimplementation runs.
The guard pattern
You've already seen guards in the standard library:
MutexGuard- holds a lock, releases it on dropRefMut- holds aRefCellborrow, releases it on dropFile- holds a file descriptor, closes it on dropTcpStream- holds a socket, closes it on drop
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
- Good uses: connection cleanup, lock management, temporary files, removing entries from registries, flushing buffers, restoring previous state
- When not to: if the only cleanup is freeing memory, you don't
need
Drop- Rust handles that automatically. ImplementDropfor side effects beyond deallocation.
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.
Telex