The Drop Trait for Cleanup

Learn how to run custom code automatically when values go out of scope in Rust, implementing destructor-like cleanup for resource management with the Drop trait

When a value in Rust goes out of scope, the compiler ensures its memory is freed. That is the ownership system’s most visible job. But memory is not the only kind of resource a program needs to release. File handles, network sockets, lock guards, and temporary buffers all need explicit cleanup—and forgetting any one of them can leak a resource until the program ends. Rust’s Drop trait gives every type a chance to run custom code precisely once, right before its value disappears. You do not call anything; the compiler inserts the call for you. This removes an entire category of mistakes where a programmer fails to clean up after themselves.

The Drop trait is the second fundamental trait behind the smart pointer pattern, alongside Deref. While Deref controls what happens when a smart pointer is used like a reference, Drop controls what happens when that pointer—or any value—is no longer needed. Many standard library types, like Box<T>, File, MutexGuard, and Rc<T>, rely on Drop to deallocate heap memory, close handles, release locks, or decrement reference counts. The pattern is called RAII—Resource Acquisition Is Initialization—and it works by tying the lifetime of a resource directly to the lifetime of a variable. When the variable goes away, the resource goes away with it, automatically and reliably.

The next section covers how to customize this behavior for your own types. You will see the trait definition, learn the rules about drop order, understand why you cannot call drop() directly, and work with explicit early cleanup and deferred cleanup through std::mem::drop and ManuallyDrop.

Customizing Drop Behavior

Not every type needs a custom Drop implementation. If all fields of a struct already handle their own cleanup—for example, a struct containing a String and a Vec<i32>—the compiler generates a drop that cleans up each field in order. You write a custom Drop only when your type manages a resource that the compiler does not know about: a raw file descriptor, a manually allocated buffer from an FFI call, or any resource whose lifecycle does not automatically map to a Rust type.

Implementing the Drop trait means defining a single method: fn drop(&mut self). The compiler calls this method exactly once, right before the value’s memory is released. This guarantee holds even if the scope ends due to a panic!, unless the program aborts before the drop can run. The trait is in the prelude, so you do not need to import it.

Implementing the Drop Trait

The trait definition is minimal:

pub trait Drop {
    fn drop(&mut self);
}

A practical first example is a type that prints a message to show exactly when drop occurs.

struct CustomSmartPointer {
    data: String,
}
impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}
fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointers created.");
}

Running this prints:

CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

The println! in drop is for demonstration; in a real type you would close a file, release a lock, or send a shutdown message. The key insight is that the programmer wrote no call to drop. The compiler inserted the call automatically when c and d went out of scope at the end of main.

Drop is not Drop:

The method you implement is drop (lowercase) on the Drop trait. The free function std::mem::drop that you call to force early cleanup is a separate entity. The method and the function share a name, but they have different roles. The compiler calls the method; you call the function only when you need explicit early drop.

Understanding Drop Order

When a scope ends, variables are dropped in the reverse order of their creation—last in, first out. In the example above, d was created after c, so d is dropped first. This reverse ordering usually matches what you want: resources acquired later are released before those acquired earlier, avoiding dependencies.

For structs with multiple fields, fields are dropped in declaration order from top to bottom. This is guaranteed by the language.

struct First;
struct Second;
struct Third;
impl Drop for First {
    fn drop(&mut self) { println!("Dropping First"); }
}
impl Drop for Second {
    fn drop(&mut self) { println!("Dropping Second"); }
}
impl Drop for Third {
    fn drop(&mut self) { println!("Dropping Third"); }
}
struct Container {
    a: First,
    b: Second,
    c: Third,
}
fn main() {
    let _container = Container { a: First, b: Second, c: Third };
    println!("Container created");
    // Output when dropped:
    // Dropping First
    // Dropping Second
    // Dropping Third
}

When a Container goes out of scope, a is dropped before b, and b before c. If your fields need to be cleaned up in a different order, you must restructure the code—for example, by wrapping fields in Option and taking them in the desired order inside drop, or by splitting the struct.

Drop order affects correctness:

If field b depends on field a during its cleanup, you must ensure a is dropped after b. Since the compiler drops fields in declaration order, declare the dependent field first. Getting this wrong leads to use-after-free in drop—a logic bug that the compiler cannot catch.

Dropping a Value Early with std::mem::drop

The compiler’s automatic drop is correct for the vast majority of code. But sometimes you need to release a resource before the end of its scope. The most common case is a lock guard: you want to release the lock early so other code can acquire it. You cannot simply call the drop method:

let c = CustomSmartPointer { data: String::from("some data") };
c.drop(); // error[E0040]: explicit use of destructor method

Rust forbids calling drop directly. If it allowed it, the compiler would still call drop automatically at the end of the scope, resulting in a double-free or double-close—the same resource freed twice. To drop a value early, you pass it to the function std::mem::drop (which is in the prelude).

let c = CustomSmartPointer { data: String::from("some data") };
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");

Output:

CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.

The function std::mem::drop is extremely simple: it takes ownership of the argument and does nothing with it. The value goes out of scope at the end of the function, so the compiler runs its Drop implementation. This is the only safe way to force an early drop.

Never call .drop() directly:

Attempting c.drop() produces compiler error E0040. If you circumvent the compiler with unsafe code and call it anyway, the value will be dropped twice at runtime, causing undefined behavior. Use drop(c) instead.

Early drop works exactly once:

After drop(c), the variable c has been moved and is no longer usable. The compiler will reject any further use of c, so you cannot accidentally read freed memory. This is the ownership system protecting you from use-after-move.

A Practical Example: Connection Pool Guard

To see Drop in a realistic scenario, consider a simple connection pool that loans out database connections and requires them to be returned after use. A guard struct can automatically return the connection when it goes out of scope, eliminating the risk of a leaked connection.

use std::sync::{Arc, Mutex};
use std::collections::VecDeque;
struct DbConnection {
    id: u32,
}
impl DbConnection {
    fn query(&self, sql: &str) -> String {
        format!("Connection {} executed: {}", self.id, sql)
    }
}
struct ConnectionPool {
    connections: Mutex<VecDeque<DbConnection>>,
}
impl ConnectionPool {
    fn new(size: u32) -> Arc<Self> {
        let mut conns = VecDeque::new();
        for id in 0..size {
            conns.push_back(DbConnection { id });
        }
        Arc::new(ConnectionPool {
            connections: Mutex::new(conns),
        })
    }
    fn get(self: &Arc<Self>) -> Option<PooledConnection> {
        let conn = self.connections.lock().unwrap().pop_front()?;
        Some(PooledConnection {
            conn: Some(conn),
            pool: Arc::clone(self),
        })
    }
    fn return_connection(&self, conn: DbConnection) {
        self.connections.lock().unwrap().push_back(conn);
    }
    fn available(&self) -> usize {
        self.connections.lock().unwrap().len()
    }
}
struct PooledConnection {
    conn: Option<DbConnection>,
    pool: Arc<ConnectionPool>,
}
impl PooledConnection {
    fn query(&self, sql: &str) -> String {
        self.conn.as_ref().unwrap().query(sql)
    }
}
impl Drop for PooledConnection {
    fn drop(&mut self) {
        if let Some(conn) = self.conn.take() {
            println!("Returning connection {} to pool", conn.id);
            self.pool.return_connection(conn);
        }
    }
}
fn main() {
    let pool = ConnectionPool::new(3);
    println!("Available connections: {}", pool.available());
    {
        let conn1 = pool.get().unwrap();
        let conn2 = pool.get().unwrap();
        println!("Available connections: {}", pool.available());
        println!("{}", conn1.query("SELECT * FROM users"));
        // conn1 and conn2 go out of scope and are automatically returned
    }
    println!("Available connections: {}", pool.available());
}

The Option<DbConnection> inside PooledConnection is a common pattern. The drop method receives &mut self, which does not provide ownership of the fields. To move the connection back to the pool, you need ownership. Wrapping it in Option and calling take() extracts the value while leaving None in its place. This ensures the connection is returned exactly once, even if drop were somehow called multiple times (which it is not, but the pattern guards against logic bugs).

ManuallyDrop - Deferring or Preventing Cleanup

Sometimes you want to prevent a value’s Drop from running entirely. The standard library provides std::mem::ManuallyDrop, a wrapper that suppresses the destructor of its inner value. This is used in low-level code where you need to transfer ownership of raw resources without double-free, such as when implementing a custom container or passing memory across FFI boundaries.

use std::mem::ManuallyDrop;
struct ExpensiveResource {
    data: Vec<u8>,
}
impl ExpensiveResource {
    fn new(size: usize) -> Self {
        println!("Allocating {} bytes", size);
        ExpensiveResource {
            data: vec![0u8; size],
        }
    }
    fn into_raw_parts(self) -> (*mut u8, usize) {
        let mut manual = ManuallyDrop::new(self);
        let ptr = manual.data.as_mut_ptr();
        let len = manual.data.len();
        (ptr, len)
    }
}
impl Drop for ExpensiveResource {
    fn drop(&mut self) {
        println!("Freeing {} bytes", self.data.len());
    }
}
fn main() {
    // Normal case: resource freed automatically
    {
        let resource = ExpensiveResource::new(1024);
        // Prints "Freeing 1024 bytes" when scope ends
    }
    // Manual case: we take raw parts and assume responsibility
    {
        let resource = ExpensiveResource::new(2048);
        let (ptr, len) = resource.into_raw_parts();
        println!("Got raw pointer, {} bytes - no automatic cleanup!", len);
        // No "Freeing..." message printed. We must manually free later.
        unsafe {
            let _ = Vec::from_raw_parts(ptr, len, len);
            // This drops the Vec, which frees the memory.
        }
    }
}

ManuallyDrop is not for everyday code. Use it only when you must transfer ownership of a resource in a way that bypasses Rust’s normal ownership tracking—for example, passing a pointer to C code or implementing a custom allocation strategy.

ManuallyDrop is dangerous:

When you wrap a value in ManuallyDrop, its Drop will never run unless you unwrap it and let the value drop normally. Forgetting to manually clean up such a value causes a resource leak. The compiler gives no warning.


This chapter covered the Drop trait, from the basic mechanism of automatic cleanup to explicit early dropping and the careful management of drop order and ManuallyDrop. The Drop trait underlies Rust’s resource management guarantees, and almost every smart pointer you use—Box<T>, Rc<T>, RefCell<T>, Mutex<T>—relies on it internally. When you implement Drop on your own types, you extend the same automatic, safe cleanup to resources the compiler does not know about.