96. Result::inspect_err — Log Errors Without Breaking the ? Chain

You want to log an error but still bubble it up with ?. The usual trick is .map_err with a closure that sneaks in a eprintln! and returns the error unchanged. Result::inspect_err does that for you — and reads like what you meant.

The problem: logging mid-chain

You’re happily propagating errors with ?, but somewhere in the pipeline you want to peek at the failure — log it, bump a metric, attach context — without otherwise touching the value:

1
2
3
4
5
6
7
8
9
fn load_config(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

fn run(path: &str) -> Result<String, std::io::Error> {
    // We want to log the error here, but still return it.
    let cfg = load_config(path)?;
    Ok(cfg)
}

Adding a println! means breaking the chain, introducing a match, or writing a no-op map_err:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# fn load_config(path: &str) -> Result<String, std::io::Error> {
#     std::fs::read_to_string(path)
# }
fn run(path: &str) -> Result<String, std::io::Error> {
    let cfg = load_config(path).map_err(|e| {
        eprintln!("failed to read {}: {}", path, e);
        e  // have to return the error unchanged — easy to mess up
    })?;
    Ok(cfg)
}

That closure always has to end with e. Miss it and you’re silently changing the error type — or worse, swallowing it.

The fix: inspect_err

Stabilized in Rust 1.76, Result::inspect_err runs a closure on the error by reference and passes the Result through untouched:

1
2
3
4
5
6
7
8
# fn load_config(path: &str) -> Result<String, std::io::Error> {
#     std::fs::read_to_string(path)
# }
fn run(path: &str) -> Result<String, std::io::Error> {
    let cfg = load_config(path)
        .inspect_err(|e| eprintln!("failed to read config: {e}"))?;
    Ok(cfg)
}

The closure takes &E, so there’s nothing to return and nothing to get wrong. The value flows straight through to ?.

The Ok side too

There’s a mirror image for the happy path: Result::inspect. Same idea — &T in, nothing out, value preserved:

1
2
3
4
5
6
let parsed: Result<u16, _> = "8080"
    .parse::<u16>()
    .inspect(|port| println!("parsed port: {port}"))
    .inspect_err(|e| eprintln!("parse failed: {e}"));

assert_eq!(parsed, Ok(8080));

Both methods exist on Option too (Option::inspect) — handy when you want to trace a Some without consuming it.

Why it’s nicer than map_err

map_err is for transforming errors. inspect_err is for observing them. Using the right tool means:

  • No accidental error swallowing — the closure can’t return the wrong type.
  • The intent is obvious at a glance: this is a side effect, not a transformation.
  • It composes cleanly with ?, and_then, and the rest of the Result toolbox.
1
2
3
4
5
6
7
# fn load_config(path: &str) -> Result<String, std::io::Error> {
#     std::fs::read_to_string(path)
# }
// Chain several observations together without ceremony.
let _ = load_config("/does/not/exist")
    .inspect(|cfg| println!("loaded {} bytes", cfg.len()))
    .inspect_err(|e| eprintln!("load failed: {e}"));

Reach for inspect_err any time you’d otherwise write map_err(|e| { log(&e); e }) — you’ll have one less footgun and one less line.

95. LazyLock::get — Peek at a Lazy Value Without Initializing It

Wanted to know whether a LazyLock has been initialized yet — without causing the initialization by touching it? Rust 1.94 stabilises LazyLock::get and LazyCell::get, which return Option<&T> and leave the closure untouched if it hasn’t fired.

The old pain

Any access that goes through Deref forces the closure to run. So the moment you do *CONFIG — or anything that implicitly derefs — the lazy becomes eager. Before 1.94 there was no way to ask “has this been initialized?” without tripping that wire.

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

static CONFIG: LazyLock<Vec<String>> = LazyLock::new(|| {
    // Imagine this is slow: reads a file, hits the network, etc.
    vec!["debug".into(), "verbose".into()]
});

fn main() {
    // No way to peek without paying the init cost
    let _ = CONFIG.len(); // forces init
    assert_eq!(CONFIG.len(), 2);
}

Handy enough, but if you want a metric like “was the config ever read?” you’d have to wrap the whole thing in your own AtomicBool.

The fix: LazyLock::get

Called as an associated function (LazyLock::get(&lock)), it returns Option<&T> without touching the closure. None means the lazy is still pending. Some(&value) means someone already forced it.

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

static CONFIG: LazyLock<Vec<String>> = LazyLock::new(|| {
    vec!["debug".into(), "verbose".into()]
});

fn main() {
    // Not yet initialised — get returns None
    assert!(LazyLock::get(&CONFIG).is_none());

    // Force init via normal deref
    assert_eq!(CONFIG.len(), 2);

    // Now get returns Some(&value)
    let peek = LazyLock::get(&CONFIG).unwrap();
    assert_eq!(peek.len(), 2);
}

Note the call style: LazyLock::get(&CONFIG), not CONFIG.get(). That’s deliberate — method-lookup on the lock itself would go through Deref, which is exactly the thing we’re trying to avoid.

Same story for LazyCell

LazyCell is the single-threaded cousin and gets the same treatment:

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

fn main() {
    let greeting = LazyCell::new(|| "Hello, Rust!".to_uppercase());

    // Not forced yet
    assert!(LazyCell::get(&greeting).is_none());

    // Deref triggers the closure
    assert_eq!(*greeting, "HELLO, RUST!");

    // Now we can peek without re-running anything
    assert_eq!(LazyCell::get(&greeting), Some(&"HELLO, RUST!".to_string()));
}

Where this shines

Two patterns fall out naturally.

Metrics and diagnostics. You want to log “did we ever load the config?” at shutdown without accidentally loading it just to find out:

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

static CACHE: LazyLock<Vec<u64>> = LazyLock::new(|| (0..1000).collect());

fn was_cache_used() -> bool {
    LazyLock::get(&CACHE).is_some()
}

fn main() {
    assert!(!was_cache_used());
    let _ = CACHE.first(); // touch it
    assert!(was_cache_used());
}

Tests. Assert that the lazy init didn’t happen on a code path that shouldn’t need it — a guarantee that was basically impossible to write before.

When to reach for it

Use LazyLock::get / LazyCell::get whenever you need to ask about initialization state without causing it. For everything else, just deref as usual — that’s still the one-liner that Just Works.

Stabilised in Rust 1.94 (March 2026).

94. ControlFlow::is_break and is_continue — Ask the Flow Which Way It Went

Got a ControlFlow back from try_fold or a visitor and just want to know which variant you’re holding? Before 1.95 you either pattern-matched or reached for matches!. Rust 1.95 adds straightforward .is_break() and .is_continue() methods.

The old pain

ControlFlow<B, C> is the enum that powers short-circuiting iterator methods like try_for_each and try_fold. Once you had one, checking which arm it was took more ceremony than you’d expect:

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

fn first_over(v: &[i32], n: i32) -> ControlFlow<i32> {
    v.iter().try_for_each(|&x| {
        if x > n { ControlFlow::Break(x) } else { ControlFlow::Continue(()) }
    })
}

fn main() {
    let flow = first_over(&[1, 2, 3, 10, 4], 5);
    // Want a bool? Reach for matches!
    let found = matches!(flow, ControlFlow::Break(_));
    assert!(found);
}

The name ControlFlow::Break also collides visually with loop break, so code like this reads a bit awkwardly.

The fix: inherent is_break / is_continue

Rust 1.95 stabilises two one-line accessors, mirroring Result::is_ok / is_err and Option::is_some / is_none:

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

fn first_over(v: &[i32], n: i32) -> ControlFlow<i32> {
    v.iter().try_for_each(|&x| {
        if x > n { ControlFlow::Break(x) } else { ControlFlow::Continue(()) }
    })
}

fn main() {
    let hit = first_over(&[1, 2, 3, 10, 4], 5);
    let miss = first_over(&[1, 2, 3], 5);

    assert!(hit.is_break());
    assert!(!hit.is_continue());

    assert!(miss.is_continue());
    assert!(!miss.is_break());
}

No pattern, no import of a macro, no wildcards.

Handy in counting and filtering

Because the methods take &self, you can pipe results straight through iterator adapters:

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

fn main() {
    let flows: Vec<ControlFlow<&'static str, i32>> = vec![
        ControlFlow::Continue(1),
        ControlFlow::Break("boom"),
        ControlFlow::Continue(2),
        ControlFlow::Break("fire"),
        ControlFlow::Continue(3),
    ];

    let breaks = flows.iter().filter(|f| f.is_break()).count();
    let continues = flows.iter().filter(|f| f.is_continue()).count();

    assert_eq!(breaks, 2);
    assert_eq!(continues, 3);
}

Previously you’d write |f| matches!(f, ControlFlow::Break(_)) — shorter, but noisier and requires you to name the variant (which means a use or a fully-qualified path).

It’s just a bool — but it’s the right bool

Nothing here is groundbreaking: you could always write a matches!. But when a method exists on Result and Option and Poll and hash iterators and even Peekable, not having one on ControlFlow meant reaching for a different idiom for no good reason. 1.95 closes that small gap.

Stabilised in Rust 1.95 (April 2026).

93. MaybeUninit Array Conversions — Build Fixed Arrays Without transmute

Ever tried to build a [T; N] element-by-element and ended up reaching for mem::transmute because [MaybeUninit<T>; N] refused to convert? Rust 1.95 stabilises safe conversions between [MaybeUninit<T>; N] and MaybeUninit<[T; N]> — no transmute, no tricks.

The old pain

You want a fully-initialised [T; N], but T isn’t Default (or the init is fallible, or expensive). The canonical pattern is:

  1. Allocate [MaybeUninit<T>; N] uninitialised.
  2. Fill each slot.
  3. Get a [T; N] out the other end.

Step 3 is where it got ugly. MaybeUninit::assume_init only works on MaybeUninit<T>, not [MaybeUninit<T>; N]. To flip the array-of-uninits into uninit-of-array, you reached for mem::transmute — which works, but leans on layout assumptions and carries a big “here be dragons” vibe.

The fix: From conversions

Rust 1.95 stabilises both directions of the conversion, so no transmute is needed:

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

fn main() {
    let arr: [MaybeUninit<u32>; 4] = [
        MaybeUninit::new(1),
        MaybeUninit::new(2),
        MaybeUninit::new(3),
        MaybeUninit::new(4),
    ];

    // [MaybeUninit<T>; N] -> MaybeUninit<[T; N]>
    let packed: MaybeUninit<[u32; 4]> = MaybeUninit::from(arr);
    let init: [u32; 4] = unsafe { packed.assume_init() };

    assert_eq!(init, [1, 2, 3, 4]);
}

MaybeUninit::from takes the array-of-uninits and gives you an uninit-of-array ready to assume_init. Safe, obvious, no layout assumption on your part.

The reverse direction

You can also go the other way — from MaybeUninit<[T; N]> back to [MaybeUninit<T>; N] — useful when you want to touch elements individually:

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

fn main() {
    let packed: MaybeUninit<[u32; 3]> = MaybeUninit::new([10, 20, 30]);

    // MaybeUninit<[T; N]> -> [MaybeUninit<T>; N]
    let unpacked: [MaybeUninit<u32>; 3] = <[MaybeUninit<u32>; 3]>::from(packed);

    let values: [u32; 3] = unsafe {
        [
            unpacked[0].assume_init(),
            unpacked[1].assume_init(),
            unpacked[2].assume_init(),
        ]
    };
    assert_eq!(values, [10, 20, 30]);
}

Plus AsRef / AsMut for free

The same release adds AsRef<[MaybeUninit<T>; N]> and AsMut<[MaybeUninit<T>; N]> (plus slice versions) for MaybeUninit<[T; N]>. That means you can borrow the uninit array as a slice without any conversion ceremony:

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

fn fill_evens(buf: &mut [MaybeUninit<u32>]) {
    for (i, slot) in buf.iter_mut().enumerate() {
        slot.write(i as u32 * 2);
    }
}

fn main() {
    let mut packed: MaybeUninit<[u32; 4]> = MaybeUninit::uninit();

    // AsMut<[MaybeUninit<T>]> — borrow as a mutable slice of slots
    fill_evens(packed.as_mut());

    let init: [u32; 4] = unsafe { packed.assume_init() };
    assert_eq!(init, [0, 2, 4, 6]);
}

The helper writes through the AsMut slice view; the caller gets a fully-initialised [u32; 4] after assume_init. No transmute, no pointer casting.

When to reach for it

This is niche — you don’t need it until you’re writing generic collection code, FFI wrappers, or allocator-like APIs that build arrays without Default. But when you do, the new conversions delete an uncomfortable transmute from your codebase and make the intent explicit.

Stabilised in Rust 1.95 (April 2026).

92. core::range — Range Types You Can Actually Copy

Ever tried to reuse a 0..=10 range and hit “use of moved value”? Rust 1.95 stabilises core::range, a new module of range types that implement Copy.

The old pain

The classic range types in std::opsRange, RangeInclusive — are iterators themselves. That means iterating them consumes them, and they can’t be Copy:

1
2
3
4
let r = 0..=5;
let a: Vec<_> = r.clone().collect();
let b: Vec<_> = r.collect(); // consumes r
// r is gone here

Every time you want to reuse a range you reach for .clone() or rebuild it. Annoying for something that looks like a pair of integers.

The fix: core::range

Rust 1.95 adds new range types in core::range (re-exported as std::range). They’re plain data — Copy, no iterator state baked in — and you turn them into an iterator on demand with .into_iter():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use core::range::RangeInclusive;

fn main() {
    let r: RangeInclusive<i32> = (0..=5).into();

    let a: Vec<i32> = r.into_iter().collect();
    let b: Vec<i32> = r.into_iter().collect(); // r is Copy, still usable

    assert_eq!(a, vec![0, 1, 2, 3, 4, 5]);
    assert_eq!(a, b);
}

No .clone(), no rebuild. r is just two numbers behind the scenes, so copying it is free.

Passing ranges around

Because the new types are Copy, you can pass them into helpers without worrying about move semantics:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use core::range::RangeInclusive;

fn sum_range(r: RangeInclusive<i32>) -> i32 {
    r.into_iter().sum()
}

fn main() {
    let r: RangeInclusive<i32> = (1..=4).into();

    assert_eq!(sum_range(r), 10);
    assert_eq!(sum_range(r), 10); // reuse: r is Copy
}

The old std::ops::RangeInclusive would have been moved by the first call.

Field access, not method calls

The new types expose their bounds as public fields — no more .start() / .end() accessors. For RangeInclusive, the upper bound is called last (emphasising that it’s included):

1
2
3
4
5
6
7
8
use core::range::RangeInclusive;

fn main() {
    let r: RangeInclusive<i32> = (10..=20).into();

    assert_eq!(r.start, 10);
    assert_eq!(r.last, 20);
}

Exclusive Range has start and end in the same way. Either one is simple plain-old-data that you can pattern-match, destructure, or read directly.

When to reach for it

Use core::range whenever you want to store, pass, or reuse a range as a value. The old std::ops ranges are still everywhere (literal syntax, slice indexing, for loops), so there’s no rush to migrate — but for library APIs that take ranges as parameters, the new types are the friendlier choice.

Stabilised in Rust 1.95 (April 2026).

91. bool::try_from — Convert Integers to Booleans Safely

Need to turn a 0 or 1 from a database, config file, or FFI boundary into a bool? bool::try_from does it safely — rejecting anything that isn’t 0 or 1. Stabilized in Rust 1.95.

The old way: match or panic

When you get integer flags from external sources — database columns, binary protocols, C APIs — you need to convert them to bool. Before 1.95, you’d write your own:

1
2
3
4
5
6
7
fn int_to_bool(v: u8) -> Result<bool, String> {
    match v {
        0 => Ok(false),
        1 => Ok(true),
        other => Err(format!("invalid bool value: {other}")),
    }
}

Or worse, you’d just do v != 0 and silently treat 42 as true, hiding data corruption.

The new way: bool::try_from

Rust 1.95 stabilizes TryFrom<{integer}> for bool across all integer types — u8, i32, u64, you name it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn main() {
    let t = bool::try_from(1u8);
    let f = bool::try_from(0i32);
    let e = bool::try_from(2u8);

    assert_eq!(t, Ok(true));
    assert_eq!(f, Ok(false));
    assert!(e.is_err());

    println!("{t:?}"); // Ok(true)
    println!("{f:?}"); // Ok(false)
    println!("{e:?}"); // Err(TryFromIntError(()))
}

Only 0 maps to false and 1 maps to true. Everything else returns Err(TryFromIntError). No silent coercion, no panics.

Real-world usage: parsing a config

Here’s where it shines — reading flags from external data:

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

fn main() {
    // Simulating values from a config file or database row
    let config: HashMap<&str, i64> = HashMap::from([
        ("verbose", 1),
        ("dry_run", 0),
        ("debug", 3),  // invalid!
    ]);

    for (key, &val) in &config {
        match bool::try_from(val) {
            Ok(flag) => println!("{key} = {flag}"),
            Err(_) => eprintln!("warning: {key} has invalid bool value: {val}"),
        }
    }
}

Instead of silently treating 3 as true, you get a clear warning and a chance to handle bad data properly.

Why not just val != 0?

The C convention of “zero is false, everything else is true” works fine when you control the data. But when parsing untrusted input — database rows, wire protocols, config files — a value of 255 probably means something went wrong. bool::try_from catches that; != 0 hides it.

One method call, no custom helpers, no silent bugs.

90. black_box — Stop the Compiler From Erasing Your Benchmarks

Your benchmark ran in 0 nanoseconds? Congratulations — the compiler optimised away the code you were trying to measure. std::hint::black_box prevents that by hiding values from the optimiser.

The problem: the optimiser is too smart

The Rust compiler aggressively eliminates dead code. If it can prove a result is never used, it simply removes the computation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn sum_range(n: u64) -> u64 {
    (0..n).sum()
}

fn main() {
    let start = std::time::Instant::now();
    let result = sum_range(10_000_000);
    let elapsed = start.elapsed();

    // Without using `result`, the compiler may skip the entire computation
    println!("took: {elapsed:?}");

    // Force the result to be "used" so the above isn't optimised out
    assert!(result > 0);
}

In release mode, the compiler can see through this and may still optimise the loop away — or even compute the answer at compile time. Your benchmark reports near-zero time, and you learn nothing.

Enter black_box

std::hint::black_box takes a value and returns it unchanged, but the compiler treats it as an opaque barrier — it can’t see through it, so it can’t optimise away whatever produced that value:

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

fn sum_range(n: u64) -> u64 {
    (0..n).sum()
}

fn main() {
    let start = std::time::Instant::now();
    let result = sum_range(black_box(10_000_000));
    let _ = black_box(result);
    let elapsed = start.elapsed();

    println!("sum = {result}, took: {elapsed:?}");
}

Two black_box calls do the trick:

  1. Wrap the input — prevents the compiler from constant-folding the argument
  2. Wrap the output — prevents dead-code elimination of the computation

Before and after

Without black_box (release mode):

1
sum = 49999995000000, took: 83ns     ← suspiciously fast

With black_box (release mode):

1
sum = 49999995000000, took: 5.612ms  ← actual work

It works on any type

black_box is generic — it works on integers, strings, structs, references, whatever:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::hint::black_box;

fn main() {
    // Hide a vector from the optimiser
    let data: Vec<i32> = black_box(vec![1, 2, 3, 4, 5]);
    let total: i32 = data.iter().sum();
    let total = black_box(total);

    assert_eq!(total, 15);
}

Micro-benchmark recipe

Here’s a minimal pattern for quick-and-dirty micro-benchmarks:

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

fn fibonacci(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

fn main() {
    let iterations = 100;
    let start = Instant::now();
    for _ in 0..iterations {
        black_box(fibonacci(black_box(30)));
    }
    let elapsed = start.elapsed();
    println!("{iterations} runs in {elapsed:?} ({:?}/iter)", elapsed / iterations);
}

Without black_box, the compiler could hoist the pure function call out of the loop or eliminate it entirely. With it, each iteration does real work.

When to use it

Reach for black_box whenever you’re timing code and the results look suspiciously fast. It’s also the foundation that benchmarking frameworks like criterion and the built-in #[bench] use under the hood.

It’s not a full benchmarking harness — for serious measurement you still want warmup, statistics, and outlier detection. But when you need a quick sanity check, black_box + Instant gets the job done.

Available since Rust 1.66 on stable.

89. cold_path — Tell the Compiler Which Branch Won't Happen

Your error-handling branch fires once in a million calls, but the compiler doesn’t know that. core::hint::cold_path lets you mark unlikely branches so the optimiser can focus on the hot path.

Why branch layout matters

Modern CPUs predict which way a branch will go and speculatively execute instructions ahead of time. When the prediction is right, execution flies. When it’s wrong, the pipeline stalls.

Compilers already try to guess which branches are hot, but they don’t always get it right — especially when both sides of an if look equally plausible from a static analysis perspective. That’s where cold_path comes in.

The basics

Call cold_path() at the start of a branch that is rarely taken:

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

fn process(value: Option<u64>) -> u64 {
    if let Some(v) = value {
        v * 2
    } else {
        cold_path();
        log_miss();
        0
    }
}

fn log_miss() {
    // imagine logging or metrics here
}

The compiler now knows the else arm is unlikely. It can lay out the machine code so the hot path (the Some arm) has no jumps, keeping it in the instruction cache and the branch predictor happy.

In match expressions

cold_path works well in match arms too — mark the rare variants:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::hint::cold_path;

fn handle_status(code: u16) -> &'static str {
    match code {
        200 => "ok",
        301 => "moved",
        404 => { cold_path(); "not found" }
        500 => { cold_path(); "server error" }
        _   => { cold_path(); "unknown" }
    }
}

Only the branches you expect to be common stay on the fast track.

Building likely and unlikely helpers

If you’ve used C/C++, you might miss __builtin_expect. With cold_path you can build the same thing:

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

#[inline(always)]
const fn likely(b: bool) -> bool {
    if !b { cold_path(); }
    b
}

#[inline(always)]
const fn unlikely(b: bool) -> bool {
    if b { cold_path(); }
    b
}

fn check_bounds(index: usize, len: usize) -> bool {
    if unlikely(index >= len) {
        panic!("out of bounds: {} >= {}", index, len);
    }
    true
}

Now you can annotate conditions directly instead of marking individual branches.

A word of caution

Misusing cold_path on a branch that actually runs often can hurt performance — the compiler will deprioritise it, and you’ll get more pipeline stalls, not fewer. Always benchmark before sprinkling hints around. Profile first, hint second.

The bottom line

cold_path is a zero-cost, zero-argument function that tells the optimiser what you already know: this branch is the exception, not the rule. It’s a small tool, but in hot loops and latency-sensitive code, it can make a measurable difference.

Stabilised in Rust 1.95 (April 2026).

88. Vec::push_mut — Push and Modify in One Step

Tired of pushing a default into a Vec and then grabbing a mutable reference to fill it in? Rust 1.95 stabilises push_mut, which returns &mut T to the element it just inserted.

The old dance

A common pattern is to push a placeholder value and then immediately mutate it. Before push_mut, you had to do an awkward two-step:

1
2
3
4
5
6
7
8
9
let mut names = Vec::new();

// Push, then index back in to mutate
names.push(String::new());
let last = names.last_mut().unwrap();
last.push_str("hello");
last.push_str(", world");

assert_eq!(names[0], "hello, world");

That last_mut().unwrap() is boilerplate — you know the element is there because you just pushed it. Worse, in more complex code the compiler can’t always prove the reference is safe, forcing you into index gymnastics.

Enter push_mut

push_mut pushes the value and hands you back a mutable reference in one shot:

1
2
3
4
5
6
7
let mut scores = Vec::new();

let entry = scores.push_mut(0);
*entry += 10;
*entry += 25;

assert_eq!(scores[0], 35);

No unwraps, no indexing, no second lookup. The borrow checker is happy because there’s a clear chain of ownership.

Building structs in-place

This really shines when you’re constructing complex values piece by piece:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#[derive(Debug, Default)]
struct Player {
    name: String,
    score: u32,
    active: bool,
}

let mut roster = Vec::new();

let p = roster.push_mut(Player::default());
p.name = String::from("Ferris");
p.score = 100;
p.active = true;

assert_eq!(roster[0].name, "Ferris");
assert_eq!(roster[0].score, 100);
assert!(roster[0].active);

Instead of building the struct fully before pushing, you push a default and fill it in. This can be handy when the final field values depend on context you only have after insertion.

Also works for insert_mut

Need to insert at a specific index? insert_mut follows the same pattern:

1
2
3
4
5
let mut v = vec![1, 3, 4];
let inserted = v.insert_mut(1, 2);
*inserted *= 10;

assert_eq!(v, [1, 20, 3, 4]);

Both methods are also available on VecDeque (push_front_mut, push_back_mut, insert_mut) and LinkedList (push_front_mut, push_back_mut).

When to reach for it

Use push_mut whenever you’d otherwise write push followed by last_mut().unwrap(). It’s one less unwrap, one fewer line, and clearer intent: push this, then let me tweak it.

Stabilised in Rust 1.95 (April 2026) — update your toolchain and give it a spin.

#087 Apr 2026

87. Atomic update — Kill the Compare-and-Swap Loop

Every Rust developer who’s written lock-free code has written the same compare_exchange loop. Rust 1.95 finally gives atomics an update method that does it for you.

The old way

Atomically doubling a counter used to mean writing a retry loop yourself:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::sync::atomic::{AtomicUsize, Ordering};

let counter = AtomicUsize::new(10);

loop {
    let current = counter.load(Ordering::Relaxed);
    let new_val = current * 2;
    match counter.compare_exchange(
        current, new_val,
        Ordering::SeqCst, Ordering::Relaxed,
    ) {
        Ok(_) => break,
        Err(_) => continue,
    }
}
// counter is now 20

It works, but it’s boilerplate — and easy to get wrong (use the wrong ordering, forget to retry, etc.).

The new way: update

1
2
3
4
5
6
use std::sync::atomic::{AtomicUsize, Ordering};

let counter = AtomicUsize::new(10);

counter.update(Ordering::SeqCst, Ordering::SeqCst, |x| x * 2);
// counter is now 20

One line. No loop. No chance of forgetting to retry on contention.

The method takes two orderings (one for the store on success, one for the load on failure) and a closure that transforms the current value. It handles the compare-and-swap retry loop internally.

It returns the previous value

Just like fetch_add and friends, update returns the value before the update:

1
2
3
4
5
6
7
use std::sync::atomic::{AtomicUsize, Ordering};

let counter = AtomicUsize::new(5);

let prev = counter.update(Ordering::SeqCst, Ordering::SeqCst, |x| x + 3);
assert_eq!(prev, 5);  // was 5
assert_eq!(counter.load(Ordering::SeqCst), 8);  // now 8

This makes it perfect for “fetch-and-modify” patterns where you need the old value.

Works on all atomic types

update isn’t just for AtomicUsize — it’s available on AtomicBool, AtomicIsize, AtomicUsize, and AtomicPtr too:

1
2
3
4
5
use std::sync::atomic::{AtomicBool, Ordering};

let flag = AtomicBool::new(false);
flag.update(Ordering::SeqCst, Ordering::SeqCst, |x| !x);
assert_eq!(flag.load(Ordering::SeqCst), true);

When to use update vs fetch_add

If your operation is a simple add, sub, or bitwise op, the specialized fetch_* methods are still better — they compile down to a single atomic instruction on most architectures.

Use update when your transformation is more complex: clamping, toggling state machines, applying arbitrary functions. Anywhere you’d previously hand-roll a CAS loop.

Summary

MethodUse when
fetch_add, fetch_or, etc.Simple arithmetic/bitwise ops
updateArbitrary transformations (Rust 1.95+)
Manual CAS loopNever again (mostly)

Available on stable since Rust 1.95.0 for AtomicBool, AtomicIsize, AtomicUsize, and AtomicPtr.