The stack has limits
You're building a linked list. You write the enum, define the node, and try to compile. The compiler rejects you with an error about the size of the type. Or you try to pass a massive buffer to a function and the program crashes with a stack overflow. The stack is a small, fast region of memory reserved for function calls and local variables. It has a hard limit. If you push too much onto it, the program dies.
The heap is the warehouse. It's huge, but accessing it costs a tiny bit of time. Box<T> is the bridge between the two. It lets you put data on the heap while keeping a small, fixed-size handle on the stack. That handle owns the data. When the handle goes away, the data gets cleaned up automatically.
Box is a claim ticket for the heap
Think of the stack as your pocket and the heap as a storage unit. You can carry a receipt in your pocket that points to a storage unit full of furniture. The receipt is small. It always takes up the same amount of space, regardless of what's in the unit. When you throw away the receipt, the storage company knows to clear the unit.
Box<T> is that receipt. It lives on the stack. It points to a value on the heap. The value can be any size. The Box is always the size of a pointer: 8 bytes on a 64-bit system. This fixed size is what saves you when the compiler needs to know how much space to reserve.
Minimal example
Here's the basic pattern. You wrap a value in Box::new. The value moves to the heap. The variable holds the Box.
fn main() {
// This integer lives on the stack. It's tiny.
let on_stack = 42;
// This integer moves to the heap. The Box holds a pointer.
let on_heap = Box::new(42);
// Deref coercion lets you use the Box like the value itself.
// No asterisk needed here.
println!("Value: {}", on_heap);
// When on_heap goes out of scope, the heap memory is freed.
}
The Box implements the Deref trait. This gives you "deref coercion." The compiler automatically inserts dereferences when needed, so you can pass a Box<T> to a function that expects a &T, or print it with println! without writing *on_heap. The syntax feels like you're holding the value, even though you're holding a pointer.
Convention aside: always use Box::new. Older Rust versions had a box keyword, but it was removed. Box::new is the standard constructor. It's a function, not a macro.
What happens under the hood
When you call Box::new(value), three things happen. First, the allocator requests memory on the heap. Second, the value moves into that memory. Third, Box::new returns a Box containing a pointer to the new location. The original value is gone from the stack. Ownership transferred.
When the Box goes out of scope, the Drop implementation runs. It reads the pointer, frees the heap memory, and destroys the pointer. The cleanup is automatic. You don't call free. You don't manage lifetimes manually. The ownership system handles it.
This move semantics is key. Box::new takes ownership of the value. You can't Box a value and keep using the original variable. The compiler enforces this with E0382 (use of moved value) if you try.
fn main() {
let data = String::from("hello");
// data moves into the Box.
let boxed = Box::new(data);
// This line would fail with E0382.
// println!("{}", data);
// The Box owns the String now.
println!("{}", boxed);
}
The Box is the sole owner. If you need multiple owners, Box is the wrong tool. Reach for Rc<T> or Arc<T> instead.
Recursive structures and the Sized trait
The most common reason to reach for Box is recursive data structures. A linked list node contains a pointer to the next node. A tree node contains pointers to children. If you try to embed the child directly, the type becomes infinitely large.
// This fails to compile.
// enum BadList {
// Cons(i32, BadList),
// Nil,
// }
The compiler rejects this because it cannot determine the size of BadList. A Cons contains a BadList, which contains a BadList, and so on. The size is infinite. The compiler needs a fixed size to allocate stack space.
Box breaks the cycle. A Box<GoodList> is always the size of a pointer, even though GoodList is recursive. The pointer has a known size. The payload can be anything.
enum GoodList {
Cons(i32, Box<GoodList>),
Nil,
}
fn main() {
// The Box pointers keep the size finite.
let list = GoodList::Cons(1, Box::new(GoodList::Cons(2, Box::new(GoodList::Nil))));
match list {
GoodList::Cons(val, _) => println!("Head: {}", val),
GoodList::Nil => println!("Empty"),
}
}
This works because Box<T> implements the Sized trait. Sized means the compiler knows the size at compile time. Pointers are always sized. The recursive type behind the pointer doesn't matter. The compiler sees a pointer and moves on.
If you forget the Box in a recursive type, the compiler emits an error about the size not being known. You'll see E0277 (the trait Sized is not implemented) or a message about infinite size. The fix is almost always to wrap the recursive field in a Box.
Trait objects and dynamic dispatch
Trait objects are another place where Box is essential. A trait object like dyn Animal has a dynamic size. The compiler doesn't know how big a Dog is versus a Cat at compile time. You can't put a dyn Animal on the stack directly.
trait Animal {
fn speak(&self);
}
struct Dog;
impl Animal for Dog {
fn speak(&self) { println!("Woof!"); }
}
fn main() {
// This fails. dyn Animal is unsized.
// let animal: dyn Animal = Dog;
// Box provides the fixed-size handle.
let animal: Box<dyn Animal> = Box::new(Dog);
// Dynamic dispatch calls the correct method.
animal.speak();
}
Box<dyn Animal> works because the Box is a pointer. The pointer size is fixed. The trait object lives on the heap, and the Box points to it. The compiler uses a vtable to dispatch method calls at runtime. This is dynamic dispatch. It's slightly slower than static dispatch, but it lets you store heterogeneous types in a collection.
Convention aside: when you see Box<dyn Trait>, read it as "a heap-allocated trait object." The Box is the handle. The dyn Trait is the payload. You'll see this pattern everywhere in Rust codebases that use polymorphism.
Deref coercion and the illusion of direct access
Box implements Deref and DerefMut. This gives you automatic dereferencing. You can call methods on the inner value without writing *.
fn main() {
let boxed_str = Box::new(String::from("hello"));
// String has a len() method. Box<String> doesn't.
// Deref coercion converts &Box<String> to &String.
println!("Length: {}", boxed_str.len());
// You can also dereference manually if needed.
println!("First char: {}", (*boxed_str).chars().next().unwrap());
}
The compiler inserts the dereference automatically. This makes Box ergonomic. You rarely need to write *boxed_var. The only time you need the asterisk is when you want to move the value out of the box, or when you're dealing with a type that doesn't play nice with coercion.
This coercion also means Box<T> works seamlessly with functions that take &T. You can pass a Box to a function expecting a reference, and the compiler handles the conversion. This is why Box feels lightweight in usage, even though it involves heap allocation.
Pitfalls and compiler errors
Box is simple, but there are traps. The most common is over-allocation. Box adds heap allocation overhead. If you Box a small integer or a tiny struct, you're paying for heap management for no reason. Stack allocation is faster for small values. Only Box when you need the heap.
Another pitfall is stack overflow with deep recursion. Box moves the data to the heap, but the stack frames still grow. If you have a recursive function that calls itself a million times, the stack will overflow even if the data is boxed. The Box saves the data, not the call stack. Use iteration or tail recursion optimization if depth is a concern.
You might also encounter E0507 (cannot move out of borrowed content) if you try to move a value out of a Box while it's borrowed. Box enforces ownership rules just like any other type. You can't move the inner value if there's an active reference.
fn main() {
let boxed = Box::new(42);
// This borrows the inner value.
let ref_val = &*boxed;
// This tries to move the value out.
// It fails with E0507 because ref_val is still borrowing.
// let val = *boxed;
println!("{}", ref_val);
}
The fix is to drop the reference before moving, or to clone the value if you need both. Ownership rules apply uniformly. Box doesn't grant special powers.
When to use Box vs alternatives
Use Box<T> for recursive data structures where the compiler needs a fixed-size pointer to break the infinite size loop. Use Box<T> for trait objects like Box<dyn Trait> to enable dynamic dispatch with a known pointer size. Use Box<T> to move large values off the stack when profiling shows stack depth is the bottleneck, though this is rare. Use Box<T> to transfer ownership of a value without copying, especially when the value is expensive to clone.
Reach for stack allocation for small, short-lived values; the overhead of heap allocation isn't worth it for integers or small structs. Reach for Vec<T> when you need a growable collection; Box<T> holds exactly one value. Reach for Rc<T> or Arc<T> when multiple owners need to share the data; Box<T> has exactly one owner. Reach for &T when you need to borrow data without taking ownership; Box<T> takes ownership.
The Box is the handle. The heap is the weight. If the compiler complains about size, Box is usually the cure. If you need sharing, look elsewhere.