Skip to content

Smart Pointers

JavaScript references vs Rust smart pointers

Section titled “JavaScript references vs Rust smart pointers”

In JavaScript and TypeScript, every object lives on the heap and the garbage collector manages its lifetime. You never think about who owns what — the GC figures it out. Rust has no GC. Instead, it gives you a toolkit of smart pointers that express ownership and aliasing rules explicitly, at zero runtime cost.

Smart pointerPurposeJS/TS analogy
Box<T>Heap allocation, single ownernew MyClass() — one owner, dropped when out of scope
Rc<T>Reference-counted shared ownership (single-thread)GC reference counting
Arc<T>Same as Rc<T> but thread-safeShared reference across async tasks
RefCell<T>Interior mutability (runtime borrow checking){ mutable: T } wrapper — you control when to borrow
Rc<RefCell<T>>Shared + mutable (single-thread)Mutable shared state across multiple holders

Box<T> — heap allocation with single ownership

Section titled “Box<T> — heap allocation with single ownership”

Box<T> is the simplest smart pointer: it puts a value on the heap and gives you a single-owner pointer to it. When the Box goes out of scope, the heap memory is freed automatically.

TypeScript
// TypeScript — every object is heap-allocated by default
class Node {
constructor(
public value: number,
public next: Node | null = null
) {}
}
const list = new Node(1, new Node(2, new Node(3)));
console.log(list.value); // 1
console.log(list.next?.value); // 2
Rust
// Box<T>: explicit heap allocation in Rust
// Required for recursive types (the compiler needs a fixed-size pointer)
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Cons(1,
Box::new(List::Cons(2,
Box::new(List::Cons(3,
Box::new(List::Nil))))));
println!("{:?}", list);
// Simple Box usage
let b = Box::new(5);
println!("b = {b}");
// b is freed here (end of scope)
}

Rc<T> — shared ownership with reference counting

Section titled “Rc<T> — shared ownership with reference counting”

When multiple parts of your program need to read the same value and you cannot determine which will be the last to use it at compile time, use Rc<T>. Every Rc::clone increments a reference count; when the last Rc drops, the value is freed. This is what JavaScript’s GC does automatically — Rc makes it explicit.

TypeScript
// TypeScript — two variables can reference the same object
const config = { maxRetries: 3 };
const serviceA = { config };
const serviceB = { config }; // same object, no copy
config.maxRetries = 5;
console.log(serviceA.config.maxRetries); // 5 — shared reference
Rust
use std::rc::Rc;
fn main() {
let config = Rc::new(String::from("max_retries=3"));
let service_a = Rc::clone(&config); // increments ref count
let service_b = Rc::clone(&config); // increments ref count
println!("config = {config}");
println!("service_a = {service_a}");
println!("service_b = {service_b}");
println!("ref count = {}", Rc::strong_count(&config)); // 3
drop(service_a); // decrements ref count
println!("after drop: ref count = {}", Rc::strong_count(&config)); // 2
}

Rust’s borrow checker enforces that you either have one mutable reference OR many immutable references — never both at once. RefCell<T> moves this check to runtime: you call .borrow() for shared access and .borrow_mut() for exclusive access. If you violate the rule at runtime, it panics instead of refusing to compile.

use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
{
let mut v = data.borrow_mut(); // exclusive borrow
v.push(4);
} // borrow released here
println!("{:?}", data.borrow()); // shared borrow: [1, 2, 3, 4]

Combine Rc (multiple owners) with RefCell (interior mutability) to get the closest equivalent to JavaScript’s mutable shared references.

use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// Box<T>: heap allocation, single owner
let b = Box::new(5);
println!("b = {b}");
// Rc<T>: reference-counted shared ownership
let a = Rc::new(String::from("hello"));
let b2 = Rc::clone(&a);
println!("a = {a}, b2 = {b2}, count = {}", Rc::strong_count(&a));
// RefCell<T>: interior mutability - borrow checking at runtime
let data = RefCell::new(vec![1, 2, 3]);
{
let mut v = data.borrow_mut();
v.push(4);
}
println!("data = {:?}", data.borrow());
// Rc<RefCell<T>>: shared mutable state
let shared = Rc::new(RefCell::new(0));
let clone1 = Rc::clone(&shared);
*clone1.borrow_mut() += 10;
println!("shared = {}", shared.borrow());
}
When do you need `Box<T>` instead of a plain value?
What does `Rc::clone(&ptr)` do?
What happens if you call `borrow_mut()` on a `RefCell<T>` when a borrow is already active?
Which combination should you use for shared mutable state across multiple threads?