#155 May 2026

155. Mutex<T> — Cross-Thread Exclusive Access, With a Guard Instead of a Panic

Yesterday’s RefCell<T> gives you &mut through &self on a single thread — and panics the moment a second borrow shows up. Mutex<T> is the same idea wearing a hard hat: it’s Sync, so it works across threads, and instead of panicking on contention it just blocks until the other holder is done.

The pain: RefCell is single-threaded, and panics on contention

RefCell<T> is !Sync. The moment you try to share one across threads, the compiler stops you:

1
2
3
4
5
6
7
8
9
use std::cell::RefCell;
use std::sync::Arc;
use std::thread;

let shared = Arc::new(RefCell::new(0u32));
let s2 = Arc::clone(&shared);

thread::spawn(move || { *s2.borrow_mut() += 1; });
// error[E0277]: `RefCell<u32>` cannot be shared between threads safely

Even on one thread, RefCell will panic if a borrow_mut() clashes with a live borrow(). That’s fine for logic bugs you want to find loudly — useless for two real threads that genuinely both want to write.

The fix: lock() returns a guard

Mutex<T> is Sync (when T: Send), and its .lock() method takes &self, blocks until the mutex is free, and hands back a MutexGuard<'_, T>. The guard derefs to &mut T, and releases the lock when it drops:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::sync::Mutex;

let m = Mutex::new(0u32);

{
    let mut g = m.lock().unwrap();   // blocks here if held; we're alone, so it's instant
    *g += 1;
    *g += 1;
}                                     // lock released as `g` drops

assert_eq!(*m.lock().unwrap(), 2);

The .unwrap() is there because lock() returns Result — see “Poisoning” below.

The classic: Arc<Mutex<T>> across threads

Mutex is the inside half. To share ownership across threads you wrap it in Arc — the thread-safe sibling of Rc (covered in Sunday’s morning bite):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0u32));
let mut handles = Vec::new();

for _ in 0..8 {
    let c = Arc::clone(&counter);
    handles.push(thread::spawn(move || {
        for _ in 0..100 {
            *c.lock().unwrap() += 1;
        }
    }));
}

for h in handles { h.join().unwrap(); }

assert_eq!(*counter.lock().unwrap(), 800);

Eight threads, a hundred increments each, eight hundred total — no torn writes, no races, because every increment runs while exactly one thread holds the lock.

The footgun: holding the guard too long

The single most common Mutex bug is leaving the guard alive across slow work. Anything between lock() and the guard going out of scope is serialized:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// BAD — lock held for the whole block, including the slow call
let g = state.lock().unwrap();
if let Some(v) = g.cache.get(&key) {
    slow_network_thing(v);            // every other thread is blocked on us
}

// BETTER — get out what you need, drop the guard, then do the slow work
let v = state.lock().unwrap().cache.get(&key).cloned();
if let Some(v) = v {
    slow_network_thing(&v);
}

drop(g) works too if you can’t easily restructure. The mental model: the guard is a lease, keep it as short as you can.

Poisoning, and why .lock() returns Result

If a thread panics while holding the lock, the Mutex is poisoned. Future lock() calls return Err(PoisonError) so you can decide whether the data is still consistent. You can always recover the inner guard with .into_inner():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::sync::{Arc, Mutex};
use std::thread;

let m = Arc::new(Mutex::new(vec![1, 2, 3]));
let m2 = Arc::clone(&m);

let _ = thread::spawn(move || {
    let _g = m2.lock().unwrap();
    panic!("oops");
}).join();                            // panic propagates, then mutex is poisoned

let mut g = match m.lock() {
    Ok(g) => g,
    Err(p) => p.into_inner(),         // we know our data is still fine
};
g.push(4);
assert_eq!(*g, vec![1, 2, 3, 4]);

For data where one panic mid-update really would leave things half-written, the Err is the signal to crash the whole component instead of papering over it.

When to pick Mutex vs RefCell vs RwLock

RefCell<T> for single-threaded interior mutability where contention is a bug. Panics loudly. Zero locking cost.

Mutex<T> when more than one thread needs to write — or might. Blocks instead of panicking. One holder at a time, readers and writers treated identically.

RwLock<T> (covered in this afternoon’s bite) when reads vastly outnumber writes and you want many readers to proceed in parallel. Pricier per-op than Mutex, but the parallelism wins for read-heavy workloads.

154. UnsafeCell<T> — The Primitive Every Interior-Mutability Type Is Built On

This morning’s OnceCell / LazyCell bite — and Cell, RefCell, Mutex, RwLock, OnceLock, every atomic — all bottom out at the same type: UnsafeCell<T>. It is the one and only legal way in Rust to mutate through a shared reference. You’ll almost never type its name, but knowing what it does explains why all the rest exist.

The pain: &T is “you may not mutate” — to the optimizer too

Outside UnsafeCell, the compiler is allowed to assume &T points to data that won’t change underneath it. It can cache loads, hoist reads out of loops, mark pointers noalias in LLVM. Try to fake interior mutability with a raw cast and you’ve signed up for undefined behavior — not “it works but is ugly,” actual UB the optimizer is free to exploit:

1
2
3
4
fn cheat(r: &u32) {
    let p = r as *const u32 as *mut u32;
    unsafe { *p = 99; } // UB: mutating through &T without UnsafeCell
}

The cast compiles. The write may even appear to happen. But the program has lost all guarantees, and the next release of rustc — or a different opt level — can break it.

The fix: UnsafeCell::get returns *mut T through &self

UnsafeCell<T> is the one type the compiler treats specially: a &UnsafeCell<T> is not a promise that the inside won’t change, so noalias and friends don’t apply. Its .get() method takes &self and hands back a raw *mut T:

1
2
3
4
5
6
use std::cell::UnsafeCell;

let cell = UnsafeCell::new(10u32);
let p: *mut u32 = cell.get();    // legal through &cell
unsafe { *p = 20; }              // legal — UnsafeCell opted out of the no-mutation rule
assert_eq!(unsafe { *cell.get() }, 20);

That’s the whole feature. Every other interior-mutability type in std is a safe wrapper around exactly this primitive plus an invariant — a borrow counter, a lock bit, an atomic flag — that justifies the unsafe block inside.

Build Cell from scratch in 20 lines

To see it in action, here’s a stripped-down Cell<T> clone. Real std::cell::Cell is more polished, but the shape is identical:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use std::cell::UnsafeCell;

struct MyCell<T> {
    value: UnsafeCell<T>,
}

impl<T: Copy> MyCell<T> {
    fn new(v: T) -> Self {
        Self { value: UnsafeCell::new(v) }
    }

    fn get(&self) -> T {
        // SAFETY: UnsafeCell makes Self !Sync, so no thread races us.
        // We never hand out a reference into the cell, so no aliased &mut exists.
        unsafe { *self.value.get() }
    }

    fn set(&self, v: T) {
        // SAFETY: same as `get`.
        unsafe { *self.value.get() = v; }
    }
}

let counter = MyCell::new(0u32);
counter.set(counter.get() + 1);
counter.set(counter.get() + 1);
assert_eq!(counter.get(), 2);

UnsafeCell does the optimizer-level work; the Copy bound plus “never lend a reference out” does the safety work. Drop either piece and the type is unsound.

The !Sync default — and how Mutex opts back in

UnsafeCell<T> is !Sync even when T: Sync. That’s deliberate: if you could share an &UnsafeCell<T> across threads with no synchronization, two threads could race on the inside and you’d be back to UB.

That’s why Cell and RefCell are !Sync (single-thread only), and why Mutex<T> carries an explicit:

1
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}

The unsafe impl is the author asserting “I added the lock; now sharing across threads is sound.” The pattern recurs in every sync interior-mutability type — RwLock (tomorrow’s afternoon bite), OnceLock, LazyLock, the atomics. The shape is always: UnsafeCell<T> + an invariant + an unsafe impl Sync.

When to reach for it yourself

In application code: basically never. Cell, RefCell, Mutex, RwLock, OnceCell, OnceLock cover every common pattern and they’re already audited.

Real reasons to hold UnsafeCell directly: writing a new lock primitive, an arena that hands out &mut slots from &self, a lock-free data structure, FFI cells that mirror an existing C struct. If you’re not building infrastructure of that kind, the right move is to use a wrapper that already wraps it — and now you know what’s in the bottom of the box.

153. OnceCell<T> — Memoize Through &self Without Wrapping in RefCell

You have a parse(&self) -> &Heavy accessor that needs to compute once and cache. &self rules out a plain field assignment. Cell needs Copy. RefCell won’t lend the inside out past .borrow(). OnceCell<T> is the missing piece — write-once, &self API, hands back a real &T that lives as long as the cell.

The pain: &self memoization is awkward

Classic shape — an immutable-looking accessor that’s expensive on first call and free afterwards:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::cell::RefCell;

struct DocSlow {
    raw: String,
    parsed: RefCell<Option<Vec<String>>>,
}

impl DocSlow {
    fn lines(&self) -> Vec<String> {
        let mut slot = self.parsed.borrow_mut();
        if slot.is_none() {
            *slot = Some(self.raw.lines().map(str::to_owned).collect());
        }
        slot.clone().unwrap() // can't return a borrow that escapes RefMut
    }
}

Two problems. We .clone() on every call because a Ref<'_, T> can’t outlive the borrow() it came from. And Option<Vec<String>> plus runtime borrow checking is overkill for “set this exactly once.”

The fix: OnceCell::get_or_init

OnceCell<T> stores at most one value. get_or_init runs the closure the first time it’s called and returns &T ever after — and that &T is tied to the lifetime of &self, so you can hand it back without cloning:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use std::cell::OnceCell;

struct Doc {
    raw: String,
    parsed: OnceCell<Vec<String>>,
}

impl Doc {
    fn new(s: &str) -> Self {
        Self { raw: s.to_owned(), parsed: OnceCell::new() }
    }

    fn lines(&self) -> &[String] {
        self.parsed
            .get_or_init(|| self.raw.lines().map(str::to_owned).collect())
    }
}

let doc = Doc::new("one\ntwo\nthree");
assert_eq!(doc.lines(), &["one", "two", "three"]);
assert_eq!(doc.lines().len(), 3); // cached — closure does not run again

No Option, no clone, no borrow_mut. The closure fires exactly once even across multiple calls, and the returned slice is good for as long as the &Doc is.

When you want to decide later, not on first read

OnceCell doesn’t require a closure at the call site. Use set when initialization is driven by something outside the cell — a parsed CLI flag, a value computed by a sibling method, anything that doesn’t fit a self-contained || ...:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::cell::OnceCell;

let cell: OnceCell<String> = OnceCell::new();
assert_eq!(cell.get(), None);

cell.set("loaded".into()).unwrap();
assert_eq!(cell.get(), Some(&"loaded".to_string()));

// Second set is rejected — the cell is full.
assert!(cell.set("nope".into()).is_err());

set returns Err(value) on the second call so you get your input back instead of dropping it on the floor. Reach for set when initialization is driven from outside; reach for get_or_init when it isn’t.

LazyCell<T, F>: when the closure is fixed at construction

If you already know how to build the value when you create the cell, LazyCell bakes the closure in and skips the Option-style API. The first deref runs it:

1
2
3
4
5
6
7
8
use std::cell::LazyCell;

let tags: LazyCell<Vec<&'static str>> = LazyCell::new(|| {
    vec!["rust", "interior-mutability"]
});

assert_eq!(tags.len(), 2);   // closure runs here
assert_eq!(tags[0], "rust"); // cached

Rule of thumb: LazyCell when there is exactly one obvious way to compute the value and you want the cell to handle it; OnceCell when you need set from outside, or different get_or_init closures at different call sites.

Thread safety

Both types are !Sync — they’re the single-thread counterparts to OnceLock / LazyLock. If a static or a field shared across threads needs this pattern, swap to the sync versions. The API shape is intentionally the same; only the guarantees (and the cost) change.

152. RefCell<T> — When You Need to Actually Borrow the Inside

This morning’s Cell<T> only lets you swap whole values in and out. The moment you want to call .push() on the Vec inside, or hand out a &str slice of the String inside, you need a real &mut — and that’s exactly what RefCell<T> gives you, just with the aliasing rules checked at runtime instead of compile time.

The pain: Cell can’t lend the inside out

Cell<T> is great until you need to do anything to the value in place:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::cell::Cell;

let log: Cell<Vec<String>> = Cell::new(Vec::new());

// We want: log.push("hit".into());
// We can't — Cell only gives us `get` (copy) and `set` (overwrite).
// `log.get_mut()` exists, but that needs `&mut Cell`, defeating the point.

// Workaround: take the Vec out, mutate, put it back.
let mut v = log.take();
v.push("hit".into());
log.set(v);

The take/mutate/replace dance works but it’s noisy, and any code that runs between take and set sees an empty Vec. For non-trivial data structures — a cache, an arena, a graph node — that “hole” is unworkable.

The fix: borrow() and borrow_mut()

RefCell<T> hands out actual references through &self. borrow() gives you a Ref<T> (deref to &T), borrow_mut() gives you a RefMut<T> (deref to &mut T):

1
2
3
4
5
6
7
8
9
use std::cell::RefCell;

let log: RefCell<Vec<String>> = RefCell::new(Vec::new());

log.borrow_mut().push("first".into());
log.borrow_mut().push("second".into());

assert_eq!(log.borrow().len(), 2);
assert_eq!(log.borrow()[0], "first");

Both methods take &self, so this works behind an Rc, inside an Fn closure, in any field of a struct you only have a shared reference to. That’s the whole point of interior mutability — &self outside, &mut inside.

The invariant: same rules, just checked at runtime

RefCell enforces the exact same aliasing rule the compiler enforces statically: many &T xor one &mut T. Try to break it and it panics:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::cell::RefCell;

let cell = RefCell::new(vec![1, 2, 3]);

let r1 = cell.borrow();
let r2 = cell.borrow();    // fine — multiple readers
assert_eq!(r1.len(), 3);
assert_eq!(r2.len(), 3);
drop((r1, r2));

let mut w = cell.borrow_mut();
w.push(4);
// cell.borrow();          // would panic: already mutably borrowed
drop(w);

assert_eq!(cell.borrow().len(), 4);

The panic message is already borrowed: BorrowMutError (or BorrowError). If you can’t guarantee statically that the borrows are well-nested, use try_borrow / try_borrow_mut — they return a Result instead of panicking, which is what you want inside a Drop impl or any code path that already might be re-entering itself.

A real pattern: shared mutable state through Rc<RefCell<_>>

The canonical use is sharing one piece of mutable state between several owners on a single thread — a cache, a config, an observer list:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Default)]
struct Cache {
    hits: u32,
    keys: Vec<String>,
}

let cache = Rc::new(RefCell::new(Cache::default()));

let writer = Rc::clone(&cache);
let reader = Rc::clone(&cache);

writer.borrow_mut().keys.push("a".into());
writer.borrow_mut().hits += 1;

let snapshot = reader.borrow();
assert_eq!(snapshot.hits, 1);
assert_eq!(snapshot.keys, vec!["a".to_string()]);

Keep borrow_mut() scopes short — release the RefMut (let it drop) before calling any code that might try to borrow the same cell again. The most common “works in tests, panics in prod” bug with RefCell is a long-lived RefMut colliding with a callback that re-enters.

When to pick RefCell vs Cell

Cell<T> if swapping or copying whole values is enough — counters, flags, small Copy state. Zero overhead, no panic risk.

RefCell<T> when the inside is a real data structure you want to call methods on in place: Vec, String, HashMap, your own structs. You pay one extra word for the borrow flag and runtime checks, but you get the full &T / &mut T API back.

For multi-threaded shared state, neither of these works — both are !Sync. Reach for Mutex<T> (covered in tomorrow’s morning bite) or RwLock<T> instead.

151. Cell<T> — Interior Mutability Without the Borrow Checker Drama

You hand the same struct to two closures and both want to bump a counter. &mut fights you, RefCell introduces runtime borrow checks you don’t need — Cell<T> quietly mutates through a shared & reference, with zero overhead, as long as you only ever swap whole values in and out.

The pain: shared & and a counter

Two closures, one counter, one immutable reference:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Counter {
    hits: u32,
}

let counter = Counter { hits: 0 };
let bump = || {
    // ERROR: cannot assign to `counter.hits`, which is behind a `&` reference
    // counter.hits += 1;
};
bump();

Fn closures only capture &, so a plain field is read-only. You could redesign for FnMut and a single owner, but the moment you have two observers or a callback registry, you need interior mutability.

The fix: Cell<T> mutates through &

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::cell::Cell;

struct Counter {
    hits: Cell<u32>,
}

let counter = Counter { hits: Cell::new(0) };

let bump = || counter.hits.set(counter.hits.get() + 1);
let read = || counter.hits.get();

bump();
bump();
bump();
assert_eq!(read(), 3);

Cell::new wraps the value; get returns a copy (so T: Copy for that method); set overwrites. Both take &self — no &mut anywhere — which is why this works inside Fn closures, inside Rc, inside any structure that hands out shared references.

The invariant: you can never borrow the inside

This is the single rule that makes Cell<T> sound without runtime checks: you cannot get a reference to the value inside, only copies and swaps. There is no cell.as_ref(), no cell.deref(). The compiler enforces this — there’s nothing to alias, so the optimizer is free to assume the inner value can’t change underneath an outstanding &T.

That’s also why Cell<T> is !Sync — fine for one thread, but two threads racing on set would tear the value. For threads, reach for Atomic* (scalars) or Mutex<T> (the rest).

replace and take work for non-Copy types too

get requires T: Copy, but Cell works with String, Vec, anything — you just have to move the value out instead of copying it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::cell::Cell;

let slot: Cell<String> = Cell::new(String::from("hello"));

// `replace` swaps in a new value, returns the old one.
let old = slot.replace(String::from("world"));
assert_eq!(old, "hello");

// `take` is `replace(Default::default())`.
let now: String = slot.take();
assert_eq!(now, "world");
assert_eq!(slot.into_inner(), ""); // default String

This is the trick people miss: Cell<Vec<T>> is perfectly usable for a shared, append-only-ish buffer — you just swap the whole Vec in and out.

When to pick Cell vs RefCell

Cell<T> if you only need to swap whole values: counters, flags, configuration knobs, small Copy state, or any field where replace/take is enough. Zero runtime overhead, no panic risk.

RefCell<T> (tomorrow afternoon’s bite) if you need to borrow the inside — call &mut self methods on a Vec in place, hand a &str slice to a caller, anything where copying or swapping the whole value would be wrong. You pay for runtime borrow tracking and risk a panic, but you get back the ability to use the value normally.

Default to Cell when it fits — it almost always does for simple shared-state tweaks, and the “no inner references” rule turns out to be exactly what you wanted anyway.

#150 May 2026

150. Vec::spare_capacity_mut — Fill a Vec From a Callback Without Zeroing It First

You reserve 4 KiB to read from a socket, and to hand the buffer over you first… write 4096 zeros. Vec::spare_capacity_mut exposes the reserved-but-uninitialized tail as &mut [MaybeUninit<u8>] so the callback writes straight into the allocation.

The pain: paying to overwrite

The intuitive fill-a-buffer pattern resizes the Vec first so the slice exists:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let mut buf: Vec<u8> = Vec::with_capacity(8);
buf.resize(8, 0); // writes 8 zeros we're about to clobber

// Pretend this is `read(fd, buf.as_mut_ptr(), buf.len())`.
fill(&mut buf);

assert_eq!(buf, b"rustbite");

fn fill(out: &mut [u8]) {
    out.copy_from_slice(b"rustbite");
}

It works, but resize walks the whole tail writing zeros that the next line overwrites — a measurable cost for big reads, and pointless for types where “zero” isn’t even a valid value.

The fix: write into the uninitialized tail

Vec::spare_capacity_mut(&mut self) -> &mut [MaybeUninit<T>] hands you a slice covering exactly capacity - len slots. Write into them, then call set_len to tell the Vec they’re now initialized:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::mem::MaybeUninit;

let mut buf: Vec<u8> = Vec::with_capacity(8);

// View the reserved-but-uninitialized tail.
let spare: &mut [MaybeUninit<u8>] = buf.spare_capacity_mut();
assert_eq!(spare.len(), 8);

// Write through the MaybeUninit pointer — no zeroing first.
for (slot, byte) in spare.iter_mut().zip(b"rustbite") {
    slot.write(*byte);
}

// Promise the Vec those 8 slots are now valid `u8`s.
unsafe { buf.set_len(8); }

assert_eq!(buf, b"rustbite");

spare_capacity_mut itself is safe — MaybeUninit<T> is the type that lets you hold “maybe garbage” without UB. The unsafe block is just the set_len call where you assert you really did initialize them.

Pairing with a real fill API

The standard pattern is “reserve, write, set_len”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use std::mem::MaybeUninit;

fn read_bytes(buf: &mut Vec<u8>, extra: usize, source: &[u8]) {
    buf.reserve(extra);

    let spare = buf.spare_capacity_mut();
    let n = extra.min(source.len()).min(spare.len());

    // Initialize the prefix we actually wrote.
    for i in 0..n {
        spare[i].write(source[i]);
    }

    // Only extend by what's been initialized.
    unsafe { buf.set_len(buf.len() + n); }
}

let mut v = vec![b'>', b' '];
read_bytes(&mut v, 8, b"rustbite");
assert_eq!(v, b"> rustbite");

The same shape works with read-style callbacks: cast the MaybeUninit<u8> slice to a raw pointer, hand it to C, and only extend len by the byte count the call returned. The bytes you didn’t write stay MaybeUninit — never read them.

When to reach for it

Reading from sockets, files, or FFI fill-style APIs into a Vec<u8> is the headline use case — every tokio and mio read path eventually bottoms out in this pattern. It’s also useful for non-Copy types where there’s no sensible default to seed with: image decoders writing Vec<Pixel>, audio decoders writing Vec<f32>, parser arenas writing Vec<Node>.

If you don’t need the spare capacity view — you’re building up element-by-element — Vec::push (or Vec::push_mut from bite 88) is still the right call. spare_capacity_mut is the tool for the moment you have an external writer that wants a flat buffer and you’d rather not pay to zero it first.

149. OnceLock::wait — Block a Thread Until Another One Initializes the Value

You have one thread loading a config and a handful of workers that can’t start until it’s ready. OnceLock::wait blocks until the value lands — no Condvar, no Mutex, no spin loop.

OnceLock<T> is a write-once cell: any number of threads can race to set it, but only the first wins. The usual reader API is get, which returns None until something has been stored:

1
2
3
4
5
6
use std::sync::OnceLock;

let cell: OnceLock<u32> = OnceLock::new();
assert_eq!(cell.get(), None);
cell.set(42).unwrap();
assert_eq!(cell.get(), Some(&42));

That’s fine when the reader can keep working without the value. But what if the reader genuinely needs it now? Before Rust 1.86 you’d reach for a Mutex<Option<T>> plus a Condvar, or spin in a loop calling get — both more code and more bugs than the problem deserves.

OnceLock::wait parks the calling thread until the cell is initialized, then hands back &T:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use std::sync::OnceLock;
use std::thread;
use std::time::Duration;

static CONFIG: OnceLock<String> = OnceLock::new();

thread::scope(|s| {
    // Producer: pretend this is loading config from disk.
    s.spawn(|| {
        thread::sleep(Duration::from_millis(20));
        CONFIG.set("rustbites=on".into()).unwrap();
    });

    // Consumers: block until the producer is done, then read.
    for _ in 0..3 {
        s.spawn(|| {
            let cfg: &String = CONFIG.wait();
            assert_eq!(cfg, "rustbites=on");
        });
    }
});

Every consumer gets back the same &StringOnceLock only ever holds one value, so the borrow is shared and lives as long as the cell does. No cloning, no Arc wrapping.

wait plays nicely with the existing init helpers. If you have a fallible initializer that some threads might run and others just want to await, mix get_or_init with wait:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::sync::OnceLock;
use std::thread;

static GREETING: OnceLock<String> = OnceLock::new();

thread::scope(|s| {
    s.spawn(|| {
        // First thread here pays the cost; others get the cached value.
        GREETING.get_or_init(|| "hello, bites".into());
    });
    s.spawn(|| {
        // This thread doesn't care who initialized — just wants the value.
        assert_eq!(GREETING.wait(), "hello, bites");
    });
});

A few things worth knowing:

  • wait blocks forever if nobody ever calls set (or get_or_init succeeds). It’s a synchronization primitive, not a timeout — pair it with thread::spawn for a producer you actually control.
  • It’s &self, so any number of threads can wait on the same cell at once.
  • OnceLock<T> requires T: Send + Sync to be shared across threads, same as Arc.

For lazy-init that runs on first read, LazyLock is still the right tool. But when initialization happens elsewhere and other threads need to pause until it’s done, wait turns a Condvar dance into one method call.

148. Result::is_ok_and — Test the Variant and the Value in One Call

Checking “is this Ok, and is the inner value positive?” usually means a match or an if let. Result::is_ok_and collapses both questions into one line.

The pattern crops up everywhere: you have a Result, and you want a bool that says “yes, it succeeded and the value passes some test”. Without help, you write this:

1
2
3
4
5
6
7
let parsed: Result<i32, &str> = "42".parse().map_err(|_| "bad input");

let big_enough = match &parsed {
    Ok(n) => *n > 10,
    Err(_) => false,
};
assert!(big_enough);

It works, but five lines for what reads as one question is a lot. Result::is_ok_and takes a closure that runs only on the Ok value, and returns false for any Err:

1
2
let parsed: Result<i32, &str> = "42".parse().map_err(|_| "bad input");
assert!(parsed.is_ok_and(|n| n > 10));

The closure receives the value by move, so it works cleanly with references too:

1
2
let res: Result<String, ()> = Ok(String::from("rustbites"));
assert!(res.as_ref().is_ok_and(|s| s.starts_with("rust")));

There’s a mirror method for the failure path. Result::is_err_and runs the predicate only on the Err value:

1
2
3
4
5
let res: Result<i32, &str> = Err("missing field: name");
assert!(res.is_err_and(|e| e.contains("name")));

let ok: Result<i32, &str> = Ok(1);
assert!(!ok.is_err_and(|_| true));  // Ok short-circuits to false

Both methods short-circuit on the wrong variant without ever calling the closure, so you can put expensive checks in the predicate without worrying about wasted work on the unhappy path.

A nice place this shines is filtering an iterator of Results:

1
2
3
4
5
6
let inputs = ["12", "hello", "7", "0", "99"];
let count = inputs
    .iter()
    .filter(|s| s.parse::<i32>().is_ok_and(|n| n > 5))
    .count();
assert_eq!(count, 3);  // "12", "7", and "99"

Same trick exists on Option as is_some_and and is_none_or — small surface area, big readability win.

147. Cell::as_array_of_cells — Mutate One Slot of a Cell-Wrapped Array

You have a Cell<[i32; 4]> and you want to bump element [2]. cell.get(), mutate the copy, cell.set(...) the whole thing back — for one slot? Cell::as_array_of_cells hands you &[Cell<i32>; 4] so each slot is its own little Cell.

The setup

Cell<T> gives you interior mutability for Copy types: &Cell<T> lets you swap the inner value through a shared reference. That’s lovely for a scalar, but the moment T is an array it becomes awkward — Cell only exposes get() and set() for the entire T:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::cell::Cell;

fn main() {
    let scores = Cell::new([10, 20, 30, 40]);

    // Want to bump index 2. The natural reach...
    // scores[2] += 1;            // can't index through Cell
    // scores.get()[2] += 1;       // mutating a temporary copy — does nothing

    // The "real" old way: copy out, mutate, copy back.
    let mut arr = scores.get();
    arr[2] += 1;
    scores.set(arr);

    assert_eq!(scores.get(), [10, 20, 31, 40]);
}

Three lines and a full-array copy in each direction — just to add 1. It also doesn’t compose: if you wanted to hand a single slot to another function, you’d have to pass the whole Cell<[i32; 4]> plus an index, and trust the callee to put the array back.

Enter as_array_of_cells

Stabilized in Rust 1.91, Cell::as_array_of_cells reinterprets &Cell<[T; N]> as &[Cell<T>; N]:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::cell::Cell;

fn main() {
    let scores = Cell::new([10, 20, 30, 40]);
    let slots: &[Cell<i32>; 4] = scores.as_array_of_cells();

    // Each element is now its own Cell — mutate one without touching the others.
    slots[2].set(slots[2].get() + 1);

    assert_eq!(scores.get(), [10, 20, 31, 40]);
}

No copy, no set of the whole array. The cast is free at runtime — Cell<T> is #[repr(transparent)] over T, so a Cell<[T; N]> and a [Cell<T>; N] have identical layout. The standard library just gives you the safe view of that fact.

Pair it with Cell::update

Cell::update is the obvious dance partner — read-modify-write in one call, on a single slot:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::cell::Cell;

fn main() {
    let scores = Cell::new([10, 20, 30, 40]);

    for slot in scores.as_array_of_cells() {
        slot.update(|n| n * 2);
    }

    assert_eq!(scores.get(), [20, 40, 60, 80]);
}

That’s the loop you actually wanted. No RefCell, no runtime borrow check, no panic risk.

Hand out a single slot

Because each element is a real &Cell<T>, you can pass one slot to another function and let it mutate just that slot — the rest of the array is untouched and the caller keeps full access:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::cell::Cell;

fn bump(slot: &Cell<i32>) {
    slot.update(|n| n + 100);
}

fn main() {
    let scores = Cell::new([10, 20, 30, 40]);
    let slots = scores.as_array_of_cells();

    bump(&slots[1]);
    bump(&slots[3]);

    assert_eq!(scores.get(), [10, 120, 30, 140]);
}

Try expressing that with cell.get() / cell.set() — you can’t, not without rebuilding the array on every call.

Slices too

There’s a sibling for unsized arrays: Cell::as_slice_of_cells turns &Cell<[T]> into &[Cell<T>]. Useful when the length isn’t known at compile time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::cell::Cell;

fn zero_out(buf: &Cell<[u8]>) {
    for slot in buf.as_slice_of_cells() {
        slot.set(0);
    }
}

fn main() {
    let buf: Cell<[u8; 5]> = Cell::new([1, 2, 3, 4, 5]);
    // &Cell<[u8; 5]> coerces to &Cell<[u8]> at the call site.
    zero_out(&buf);
    assert_eq!(buf.get(), [0, 0, 0, 0, 0]);
}

And as of Rust 1.95, both views also implement AsRef, so generic code can take impl AsRef<[Cell<T>]> and accept either form.

The signatures

1
2
3
4
5
6
7
impl<T, const N: usize> Cell<[T; N]> {
    pub const fn as_array_of_cells(&self) -> &[Cell<T>; N];
}

impl<T> Cell<[T]> {
    pub fn as_slice_of_cells(&self) -> &[Cell<T>];
}

Both are zero-cost reinterpretations — pure type-system moves, no copying. Reach for them any time you find yourself doing the get / mutate / set two-step on a Cell that wraps a collection.

#146 May 2026

146. char::MAX_LEN_UTF8 — Size UTF-8 Buffers Without Magic Numbers

Every time you’ve called char::encode_utf8, you’ve written [0u8; 4] from memory. Rust 1.93 stabilises char::MAX_LEN_UTF8 so you don’t have to keep that magic number in your head.

The magic number you keep typing

encode_utf8 writes the UTF-8 bytes of a char into a &mut [u8] and returns a &mut str pointing at the written portion. The slice has to be big enough — which means knowing that the worst-case UTF-8 encoding is 4 bytes:

1
2
3
let mut buf = [0u8; 4]; // why 4? because UTF-8, that's why
let s = '🦀'.encode_utf8(&mut buf);
assert_eq!(s, "🦀");

That 4 is correct but unexplained. Anyone reading your code has to either trust you or go re-derive the UTF-8 spec.

The named version

Rust 1.93 stabilises two constants on char:

1
2
assert_eq!(char::MAX_LEN_UTF8, 4);
assert_eq!(char::MAX_LEN_UTF16, 2);

MAX_LEN_UTF8 is the maximum number of u8s encode_utf8 can ever write. MAX_LEN_UTF16 is the same for encode_utf16 (a surrogate pair = 2 u16s). Drop them straight into your buffer declarations:

1
2
3
4
5
6
7
8
let mut buf = [0u8; char::MAX_LEN_UTF8];
let s = '🦀'.encode_utf8(&mut buf);
assert_eq!(s, "🦀");
assert_eq!(s.len(), 4);

let mut wide = [0u16; char::MAX_LEN_UTF16];
let w = '🦀'.encode_utf16(&mut wide);
assert_eq!(w.len(), 2);

Same behaviour, but the intent is self-documenting — the buffer is sized to hold exactly one char, by definition.

Sizing a buffer for N chars

Where this really pays off is when you’re computing a buffer for several chars on the stack:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const N: usize = 8;
let mut buf = [0u8; N * char::MAX_LEN_UTF8];

let mut pos = 0;
for c in ['h', 'é', 'l', 'l', 'o'] {
    let s = c.encode_utf8(&mut buf[pos..]);
    pos += s.len();
}

assert_eq!(&buf[..pos], "héllo".as_bytes());

Now if Unicode ever expanded its scalar value range and MAX_LEN_UTF8 grew, your code would still be correct. With a hardcoded 4, you’d have a silent buffer overflow waiting to happen the day someone bumps the constant.

Why bother?

It’s a small change — one constant, no new behaviour. But it kills a real source of off-by-one bugs (people writing [0u8; 3] because they “only handle Latin-1”) and makes UTF-8 buffer code legible at a glance. Available since Rust 1.93 (January 2026).