Box<T>: Heap Allocation
18 min read
Box<T> is the simplest smart pointer in Rust: a single owner of a value that lives on the heap instead of the stack. It is the tool you reach for when a type’s size cannot be known at compile time (recursive types), or when you want to store a value of an unknown concrete type behind a trait.
Quick Overview
Section titled “Quick Overview”In JavaScript and TypeScript, every object, array, closure, and class instance already lives on the heap behind a reference — the engine decides, and you never write the allocation. Rust flips that default: values live on the stack unless you explicitly ask for the heap. Box<T> is that explicit ask. It is a thin, owning pointer to a heap allocation that frees itself automatically when it goes out of scope — no garbage collector, no free().
You will use Box<T> mainly for three things: recursive data structures (linked lists, trees, ASTs), trait objects (Box<dyn Trait>), and occasionally to move a large value to the heap so it is cheap to pass around.
TypeScript/JavaScript Example
Section titled “TypeScript/JavaScript Example”In TypeScript, heap allocation is invisible. Every node of a linked list, every tree, every polymorphic shape is “just an object,” and the runtime quietly boxes everything onto the heap and reference-counts it for the garbage collector.
// TypeScript — a recursive linked list. Heap allocation is implicit.type List = | { kind: "cons"; value: number; rest: List } | { kind: "nil" };
const list: List = { kind: "cons", value: 1, rest: { kind: "cons", value: 2, rest: { kind: "cons", value: 3, rest: { kind: "nil" } }, },};
function sum(list: List): number { return list.kind === "cons" ? list.value + sum(list.rest) : 0;}
console.log(sum(list)); // 6
// Polymorphism is also implicit — an array of "any Shape".interface Shape { area(): number; name(): string;}
class Circle implements Shape { constructor(private radius: number) {} area() { return Math.PI * this.radius ** 2; } name() { return "circle"; }}
const shapes: Shape[] = [new Circle(2)];console.log(shapes[0].area()); // 12.566...Key points:
rest: Listself-references with no special syntax — the engine stores a pointer to another heap object.Shape[]holds heterogeneous objects; the runtime dispatches.area()dynamically.- You never decide stack vs. heap; the JavaScript engine always heap-allocates objects.
Rust Equivalent
Section titled “Rust Equivalent”Rust requires you to opt into the heap. A self-referential enum would have infinite size on the stack, so each recursive child goes behind a Box.
// Rust — a recursive cons list. `Box` provides the heap indirection.#[derive(Debug)]enum List { Cons(i32, Box<List>), Nil,}
use List::{Cons, Nil};
fn sum(list: &List) -> i32 { match list { Cons(value, rest) => value + sum(rest), Nil => 0, }}
fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); println!("{list:?}"); println!("sum = {}", sum(&list));}Output:
Cons(1, Cons(2, Cons(3, Nil)))sum = 6The Box::new(...) call allocates on the heap and returns an owning pointer. When list drops at the end of main, the whole chain is freed recursively — deterministically, with no garbage collector.
Detailed Explanation
Section titled “Detailed Explanation”What a Box<T> actually is
Section titled “What a Box<T> actually is”A Box<T> is a pointer-sized value (8 bytes on a 64-bit machine) stored on the stack, pointing at a T stored on the heap. That is the entire data structure — there is no reference count and no extra metadata (for Sized types). It is the lowest-overhead way to put something on the heap.
fn main() { let boxed: Box<i32> = Box::new(42); println!("boxed value = {}", *boxed); // explicit dereference println!("boxed value = {}", boxed); // Box<T> forwards Display to its inner T}Output:
boxed value = 42boxed value = 42The *boxed syntax dereferences the box to reach the i32 it owns. Because Box<T> implements the Deref trait, you rarely need the explicit *: method calls, field access, and most trait usage automatically “see through” the box (this is deref coercion, covered in the Deref trait topic of this section).
fn main() { let boxed = Box::new(vec![1, 2, 3]); println!("len = {}", boxed.len()); // auto-deref: (*boxed).len() println!("first = {:?}", boxed.first()); // auto-deref let total: i32 = boxed.iter().sum(); println!("sum = {total}");}Output:
len = 3first = Some(1)sum = 6Why recursive types need a Box
Section titled “Why recursive types need a Box”When the Rust compiler lays out a type, it needs to know its exact size in bytes at compile time. Consider this naive definition:
enum List { Cons(i32, List), // does not compile (error[E0072]: recursive type has infinite size) Nil,}To size List, the compiler must size Cons, which contains a List, which contains a Cons, which contains a List… forever. The size equation has no solution. The real compiler error spells this out:
error[E0072]: recursive type `List` has infinite size --> src/main.rs:1:1 |1 | enum List { | ^^^^^^^^^2 | Cons(i32, List), // no indirection | ---- recursive without indirection |help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle |2 | Cons(i32, Box<List>), // no indirection | ++++ +The compiler literally suggests the fix: wrap the recursive field in a Box. A Box<List> is always one pointer wide regardless of how deep the list goes, so List now has a finite, known size. We can confirm it:
use std::mem;
enum List { Cons(i32, Box<List>), Nil,}
fn main() { println!("size of Box<List> = {}", mem::size_of::<Box<List>>()); println!("size of List = {}", mem::size_of::<List>()); println!("size of i32 = {}", mem::size_of::<i32>());}Output:
size of Box<List> = 8size of List = 16size of i32 = 4List is 16 bytes: 4 for the i32, 8 for the Box pointer, plus padding/the discriminant — and crucially, fixed, no matter how long the actual list is. The heap holds the rest.
Note: Only the recursion needs a box.
Vec<T>,String,HashMap, and similar collections already store their contents on the heap internally, so aVec<Tree>is fine without an explicitBoxaround each element. You add aBoxwhen a single field directly contains another instance of the same type.
Trait objects in a Box
Section titled “Trait objects in a Box”The second classic use is storing a value whose concrete type you do not know at compile time, but which implements a known trait. Box<dyn Trait> is the owning version of a trait object.
trait Shape { fn area(&self) -> f64; fn name(&self) -> &str;}
struct Circle { radius: f64 }struct Rectangle { width: f64, height: f64 }
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } fn name(&self) -> &str { "circle" }}
impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height } fn name(&self) -> &str { "rectangle" }}
// One function, two different concrete return types — only possible behind `dyn`.fn make_shape(kind: &str) -> Box<dyn Shape> { match kind { "circle" => Box::new(Circle { radius: 2.0 }), _ => Box::new(Rectangle { width: 3.0, height: 4.0 }), }}
fn main() { let shapes: Vec<Box<dyn Shape>> = vec![ make_shape("circle"), make_shape("rectangle"), ];
for shape in &shapes { println!("{} has area {:.2}", shape.name(), shape.area()); }}Output:
circle has area 12.57rectangle has area 12.00A Circle and a Rectangle have different sizes, so Vec<Box<dyn Shape>> cannot store them inline — but a Box<dyn Shape> is a fixed-size fat pointer (data pointer + vtable pointer), so a Vec of them works. This is the closest Rust equivalent to TypeScript’s Shape[]. The dyn keyword and the full mechanics of dynamic dispatch are covered in ../09-generics-traits/06_trait-objects.md; here the point is simply that Box is what owns the trait object.
Tip: A function can only return one concrete type with
impl Trait. When you need to return different concrete types from different branches — asmake_shapedoes — you must useBox<dyn Trait>.
Key Differences
Section titled “Key Differences”| Concept | TypeScript / JavaScript | Rust with Box<T> |
|---|---|---|
| Where objects live | Always heap (engine-managed) | Stack by default; heap only via Box/collections |
| Who frees memory | Garbage collector, eventually | Deterministic drop at end of scope |
| Pointer cost | Hidden reference + GC bookkeeping | One machine word, zero runtime bookkeeping |
| Recursive types | rest: List “just works” | Must break the cycle with Box/Rc/& |
| Polymorphic container | Shape[] (implicit boxing) | Vec<Box<dyn Shape>> (explicit) |
| Ownership | Shared by default (multiple refs) | Single owner; move semantics |
| Null | null / undefined allowed | No null; absence is Option<Box<T>> |
The mental shift for a TypeScript developer: in JavaScript everything is a Box you never see. In Rust, the stack is the default and the heap is a deliberate choice. That choice is cheap (Box is the minimal smart pointer), but it is yours to make.
Box<T> vs. the other smart pointers
Section titled “Box<T> vs. the other smart pointers”Box<T> gives single ownership and no extra capabilities — it is purely “this value, but on the heap.” When you need shared ownership, reach for Rc/Arc. When you need to mutate through a shared pointer, reach for RefCell/Mutex or Cell. A decision guide on which smart pointer to pick lives in this section’s overview.
Common Pitfalls
Section titled “Common Pitfalls”Pitfall 1: Forgetting indirection in a recursive type
Section titled “Pitfall 1: Forgetting indirection in a recursive type”The error message is friendly, but new Rustaceans still get tripped up: defining Cons(i32, List) fails with error[E0072]: recursive type has infinite size. The fix is the compiler’s own suggestion — wrap the recursive field in Box<List> (or Rc/&). See the verified message in the Detailed Explanation above.
Pitfall 2: Trying to move a value out of a borrowed Box
Section titled “Pitfall 2: Trying to move a value out of a borrowed Box”Coming from JavaScript, where copying a reference is free, you might try to “take” a boxed value through a shared reference:
fn main() { let boxed = Box::new(String::from("hello")); let reference = &boxed; let moved = *reference; // does not compile (error[E0507]) println!("{moved}");}The real compiler error:
error[E0507]: cannot move out of `*reference` which is behind a shared reference --> src/main.rs:4:17 |4 | let moved = *reference; // cannot move out of `*reference` | ^^^^^^^^^^ move occurs because `*reference` has type `Box<String>`, which does not implement the `Copy` trait |help: consider removing the dereference here |4 - let moved = *reference; // cannot move out of `*reference`4 + let moved = reference; // cannot move out of `*reference` |help: consider cloning the value if the performance cost is acceptable |4 - let moved = *reference; // cannot move out of `*reference`4 + let moved = reference.clone(); // cannot move out of `*reference` |You cannot move ownership out through a shared &. Either borrow the inner value (let moved: &String = reference;), clone it (reference.clone()), or — if you own the box outright — dereference the owned box directly (next pitfall).
Pitfall 3: Surprise — you can move out of an owned Box
Section titled “Pitfall 3: Surprise — you can move out of an owned Box”Box<T> is special: unlike most types, dereferencing an owned box (a “deref move”) moves the value out and frees the box.
fn main() { let boxed = Box::new(String::from("hello")); let owned: String = *boxed; // moves the String off the heap into `owned` println!("{owned}"); // prints: hello // `boxed` is now consumed; using it here would be a compile error.}This works because the compiler knows boxed is the unique owner. It does not work through a reference (Pitfall 2). If you only have a reference, clone instead.
Pitfall 4: Reaching for Box when you don’t need the heap
Section titled “Pitfall 4: Reaching for Box when you don’t need the heap”A common over-correction is boxing small values “to be safe.” A plain i32, a small struct, or a fixed-size array belongs on the stack — boxing it just adds a pointer indirection and a heap allocation for no benefit. Use Box for recursion, trait objects, or genuinely large values you want to move cheaply — not as a default.
Best Practices
Section titled “Best Practices”- Box the recursion, not the leaves. Put
Boxonly on the field that creates the cycle. LetVec,String, and other collections do their own heap management. - Prefer
Option<Box<T>>for optional children. A tree node’sleft/rightare naturallyOption<Box<TreeNode>>—Noneis the empty subtree, with no null pointer in sight. - Use
Box<dyn Trait>for heterogeneous collections and “factory” return types. When branches return different concrete types,Box<dyn Trait>is the idiomatic answer;impl Traitcannot do it. - Reach for
Box<dyn Error>in application code.Box<dyn std::error::Error>is the standard “any error” return type formainand glue code (see ../08-error-handling/README.md). - Don’t box what fits on the stack. Boxing a
Copyscalar or a small struct usually pessimizes performance. - Let
Dropdo the work. Never write manual cleanup; aBoxfrees its heap allocation automatically and deterministically when it leaves scope.
Real-World Example
Section titled “Real-World Example”A production-flavored use of Box is an abstract syntax tree (AST) — for example, evaluating an arithmetic expression. Each operator node holds sub-expressions of arbitrary depth, so the children live behind Box. This is exactly the shape a calculator, query parser, or template engine uses internally.
// An arithmetic expression AST. Each operator node owns boxed sub-expressions,// so an expression of any depth has a fixed-size root node.#[derive(Debug)]enum Expr { Number(f64), Add(Box<Expr>, Box<Expr>), Sub(Box<Expr>, Box<Expr>), Mul(Box<Expr>, Box<Expr>), Div(Box<Expr>, Box<Expr>),}
impl Expr { fn eval(&self) -> Result<f64, String> { match self { Expr::Number(n) => Ok(*n), Expr::Add(a, b) => Ok(a.eval()? + b.eval()?), Expr::Sub(a, b) => Ok(a.eval()? - b.eval()?), Expr::Mul(a, b) => Ok(a.eval()? * b.eval()?), Expr::Div(a, b) => { let divisor = b.eval()?; if divisor == 0.0 { Err("division by zero".to_string()) } else { Ok(a.eval()? / divisor) } } } }}
// A small helper keeps tree construction readable.fn num(n: f64) -> Box<Expr> { Box::new(Expr::Number(n))}
fn main() { // (2 + 3) * (10 - 4) => 30 let expr = Expr::Mul( Box::new(Expr::Add(num(2.0), num(3.0))), Box::new(Expr::Sub(num(10.0), num(4.0))), ); match expr.eval() { Ok(result) => println!("result = {result}"), Err(e) => println!("error: {e}"), }
// 1 / 0 => error, propagated via `?` let bad = Expr::Div(num(1.0), num(0.0)); match bad.eval() { Ok(result) => println!("result = {result}"), Err(e) => println!("error: {e}"), }}Output:
result = 30error: division by zeroThe recursion in eval mirrors the recursion in the type, and the ? operator threads Result errors up through the tree. Because each child is owned by its parent Box, dropping the root drops the entire tree exactly once.
Further Reading
Section titled “Further Reading”Official documentation
Section titled “Official documentation”- The Rust Book — Using
Box<T>to Point to Data on the Heap - The Rust Book — Smart Pointers (chapter 15)
std::boxed::BoxAPI docs- Rust by Example — Box, stack and heap
- The Rustonomicon — “Too Many Linked Lists” (deep dive on Box-based lists)
Related topics in this guide
Section titled “Related topics in this guide”- Section 10 overview — the full map of smart pointers
- rc-arc.md — when you need shared ownership instead of a single owner
- refcell-mutex.md — mutating through a shared pointer (interior mutability)
- The
Dereftrait topic of this section — the trait that makes*boxedand auto-deref work (forthcoming) - This section’s overview — decision guide: which smart pointer to pick when
- ../09-generics-traits/06_trait-objects.md — the full story on
dyn Traitand dynamic dispatch - ../05-ownership/README.md — ownership and move semantics, the foundation
Boxbuilds on - ../11-async/README.md —
Box::pinandPin<Box<dyn Future>>show up when working with async
Exercises
Section titled “Exercises”Exercise 1: A stack as a singly linked list
Section titled “Exercise 1: A stack as a singly linked list”Difficulty: Beginner
Objective: Build a recursive data structure with Box and understand why the indirection is required.
Instructions: Implement a Stack of i32 backed by a singly linked list. Use an enum Link { Empty, More(Box<Node>) } where Node holds a value and a next: Link. Provide new, push, and pop. (Hint: std::mem::replace(&mut self.head, Link::Empty) lets you take ownership of the current head without leaving a hole.)
enum Link { Empty, More(Box<Node>),}
struct Node { value: i32, next: Link,}
struct Stack { head: Link,}
impl Stack { fn new() -> Self { // TODO } fn push(&mut self, value: i32) { // TODO } fn pop(&mut self) -> Option<i32> { // TODO }}
fn main() { let mut stack = Stack::new(); stack.push(1); stack.push(2); stack.push(3); while let Some(v) = stack.pop() { print!("{v} "); // expected: 3 2 1 } println!();}Solution
enum Link { Empty, More(Box<Node>),}
struct Node { value: i32, next: Link,}
struct Stack { head: Link,}
impl Stack { fn new() -> Self { Stack { head: Link::Empty } }
fn push(&mut self, value: i32) { // Take the old head out, then make a new node point at it. let new_node = Box::new(Node { value, next: std::mem::replace(&mut self.head, Link::Empty), }); self.head = Link::More(new_node); }
fn pop(&mut self) -> Option<i32> { match std::mem::replace(&mut self.head, Link::Empty) { Link::Empty => None, Link::More(node) => { self.head = node.next; // move the tail back into head Some(node.value) } } }}
fn main() { let mut stack = Stack::new(); stack.push(1); stack.push(2); stack.push(3); while let Some(v) = stack.pop() { print!("{v} "); } println!();}Output:
3 2 1std::mem::replace is the key trick: it swaps in Link::Empty and hands you the previous value to own. Without it, you would be trying to move self.head out of a &mut self, which the borrow checker rejects.
Exercise 2: A JSON-like value tree
Section titled “Exercise 2: A JSON-like value tree”Difficulty: Intermediate
Objective: Model a recursive document format and write a function that walks it.
Instructions: Define an enum Json with variants Null, Bool(bool), Number(f64), Str(String), Array(Vec<Json>), and Tagged(String, Box<Json>) (a label wrapping one nested value — this is the variant that requires an explicit Box). Write fn depth(value: &Json) -> usize returning the maximum nesting depth (a scalar is depth 0; each Array/Tagged layer adds 1).
Solution
#[derive(Debug)]enum Json { Null, Bool(bool), Number(f64), Str(String), Array(Vec<Json>), Tagged(String, Box<Json>),}
fn depth(value: &Json) -> usize { match value { Json::Array(items) => 1 + items.iter().map(depth).max().unwrap_or(0), Json::Tagged(_, inner) => 1 + depth(inner), _ => 0, // Null, Bool, Number, Str are leaves }}
fn main() { let doc = Json::Array(vec![ Json::Number(1.0), Json::Tagged( "nested".to_string(), Box::new(Json::Array(vec![Json::Bool(true), Json::Null])), ), Json::Str("hi".to_string()), ]); println!("depth = {}", depth(&doc)); println!("{doc:?}");}Output:
depth = 3Array([Number(1.0), Tagged("nested", Array([Bool(true), Null])), Str("hi")])Note that Array(Vec<Json>) does not need a Box — Vec already heap-allocates its elements. Only Tagged, which holds a single nested Json directly, needs the Box to break the size cycle.
Exercise 3: A command queue of boxed trait objects
Section titled “Exercise 3: A command queue of boxed trait objects”Difficulty: Intermediate / Advanced
Objective: Use Box<dyn Trait> to store and run a heterogeneous list of behaviors — the Rust analog of an array of polymorphic objects in TypeScript.
Instructions: Define a trait Command with fn run(&self, state: &mut i32) and fn describe(&self) -> String. Implement it for three unit/tuple structs: Add(i32), Mul(i32), and Reset. Write fn run_all(commands: &[Box<dyn Command>]) -> i32 that starts from 0, applies each command in order, prints "<description> -> <state>" after each, and returns the final state.
Solution
trait Command { fn run(&self, state: &mut i32); fn describe(&self) -> String;}
struct Add(i32);struct Mul(i32);struct Reset;
impl Command for Add { fn run(&self, state: &mut i32) { *state += self.0; } fn describe(&self) -> String { format!("add {}", self.0) }}
impl Command for Mul { fn run(&self, state: &mut i32) { *state *= self.0; } fn describe(&self) -> String { format!("mul {}", self.0) }}
impl Command for Reset { fn run(&self, state: &mut i32) { *state = 0; } fn describe(&self) -> String { "reset".to_string() }}
fn run_all(commands: &[Box<dyn Command>]) -> i32 { let mut state = 0; for command in commands { command.run(&mut state); println!("{:<8} -> {state}", command.describe()); } state}
fn main() { let program: Vec<Box<dyn Command>> = vec![ Box::new(Add(5)), Box::new(Mul(3)), Box::new(Add(1)), Box::new(Reset), Box::new(Add(42)), ]; let final_state = run_all(&program); println!("final = {final_state}");}Output:
add 5 -> 5mul 3 -> 15add 1 -> 16reset -> 0add 42 -> 42final = 42The Vec<Box<dyn Command>> stores three different concrete types behind one fixed-size pointer type, exactly like a TypeScript Command[]. The difference: in Rust the boxing is explicit, ownership is single and clear, and each command is freed deterministically when the Vec drops.