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).

#145 May 2026

145. Duration::from_nanos_u128 — Round-Trip Nanoseconds Without the u64 Cast

Duration::as_nanos() hands you a u128. Duration::from_nanos() takes a u64. You feed one into the other and the compiler yells at you — or worse, you cast and quietly truncate at 584 years. Rust 1.93 closed the loop with from_nanos_u128.

The mismatched-types papercut

The old API was asymmetric. Going from Duration to nanos was 128-bit:

1
2
3
4
5
use std::time::Duration;

let d = Duration::new(7, 250);
let n: u128 = d.as_nanos();
assert_eq!(n, 7_000_000_250);

Coming back, though, you only got from_nanos(_: u64) — so the round-trip needed a cast:

1
2
3
4
5
use std::time::Duration;

let n: u128 = Duration::new(7, 250).as_nanos();
let back = Duration::from_nanos(n as u64); // narrowing cast, fingers crossed
assert_eq!(back, Duration::new(7, 250));

That as u64 silently truncates anything past u64::MAX — and u64::MAX nanoseconds is roughly 584 years. Inside a calendar app you’ll never notice. Inside a scientific or simulation context, you absolutely will.

from_nanos_u128 matches as_nanos

Rust 1.93 stabilised Duration::from_nanos_u128, a const fn that takes the full 128-bit value:

1
2
3
4
5
use std::time::Duration;

let n: u128 = Duration::new(7, 250).as_nanos();
let back = Duration::from_nanos_u128(n);
assert_eq!(back, Duration::new(7, 250));

Same shape on both sides. No cast, no truncation, no silent wraparound.

Past the 584-year ceiling

Where the new constructor actually earns its keep is when you have nanoseconds counts that wouldn’t fit in a u64:

1
2
3
4
5
6
7
8
9
use std::time::Duration;

// 10^24 ns is ~31.7 million years — well past u64::MAX nanos
let nanos: u128 = 10_u128.pow(24) + 321;
let d = Duration::from_nanos_u128(nanos);

assert_eq!(d.as_secs(), 10_u64.pow(15));
assert_eq!(d.subsec_nanos(), 321);
assert_eq!(d.as_nanos(), nanos); // exact round-trip

Duration itself stores (u64 seconds, u32 nanos), so it has plenty of room — the old from_nanos was just bottlenecked by its argument type.

One thing to watch

from_nanos_u128 panics if you hand it more than Duration::MAX worth of nanoseconds. If you’re pulling values from user input or untrusted sources, guard the upper bound yourself — there isn’t a checked_from_nanos_u128 (yet).

When to reach for it

Use from_nanos_u128 whenever you already have a u128 of nanoseconds — typically because it came out of as_nanos, an arithmetic accumulator, or a high-precision external clock. Stick with the plain from_nanos(_: u64) for short-lived timeouts and durations measured in milliseconds or seconds; the u64 is plenty.

Stabilised in Rust 1.93 (January 2026). Available as const fn, so it works in const contexts too.

#143 May 2026

143. Vec::dedup_by_key — Collapse Consecutive Duplicates by a Derived Key

Vec::dedup() only collapses runs that are exactly equal. When you care about a derived attribute — the minute on a timestamp, the domain in an email, the first letter of a word — reach for dedup_by_key.

The plain dedup() is strict: two adjacent elements are only merged if == says so.

1
2
3
let mut nums = vec![1, 1, 2, 3, 3, 3, 2, 5];
nums.dedup();
assert_eq!(nums, vec![1, 2, 3, 2, 5]);

But often “duplicate” really means “shares some property with its neighbour.” dedup_by_key takes a closure that maps each element to a key, then keeps the first of every consecutive run whose keys match:

1
2
3
let mut nums = vec![1, 3, 5, 2, 4, 7, 6, 8];
nums.dedup_by_key(|n| *n % 2);
assert_eq!(nums, vec![1, 2, 7, 6]);

1, 3, 5 all have key 1 → keep 1. Then 2, 4 have key 0 → keep 2. Then 7 has key 1 → keep it. Then 6, 8 have key 0 → keep 6.

The practical case: log lines already in time order, and you want one representative per minute.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let mut lines = vec![
    "12:01 GET /a".to_string(),
    "12:01 GET /b".to_string(),
    "12:01 POST /c".to_string(),
    "12:02 GET /d".to_string(),
    "12:02 GET /e".to_string(),
    "12:03 PATCH /f".to_string(),
];

lines.dedup_by_key(|line| line[..5].to_string());

assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "12:01 GET /a");
assert_eq!(lines[1], "12:02 GET /d");
assert_eq!(lines[2], "12:03 PATCH /f");

Two things worth remembering. First, it only looks at adjacent pairs — if you need full uniqueness, pair it with sort_by_key first. Second, if your equivalence isn’t expressible as a key (e.g. “values within 0.1 of each other”), there’s a sibling dedup_by that takes |a, b| -> bool directly. All three are in-place, allocation-free, and run in linear time.