#274 Jul 22, 2026

274. fetch_update — The CAS Loop You Keep Writing by Hand

There’s no fetch_add variant that stops at a cap, so you write a compare_exchange_weak loop by hand. fetch_update is that loop, done right, in one call.

The Atomic* types ship fetch_add, fetch_or, fetch_min — but the moment your update rule isn’t one of those, you’re hand-rolling a compare-and-swap loop. A rate limiter that counts hits but saturates at a cap:

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

const CAP: u32 = 3;
let hits = AtomicU32::new(0);

let mut cur = hits.load(Ordering::Relaxed);
while cur < CAP {
    match hits.compare_exchange_weak(
        cur,
        cur + 1,
        Ordering::Relaxed,
        Ordering::Relaxed,
    ) {
        Ok(_) => break,
        Err(actual) => cur = actual,
    }
}

Easy to get subtly wrong: forget to reload on failure, spin forever, or check the cap on the stale value. fetch_update owns the loop — you supply only the transform, returning Some(new) to store or None to leave it alone:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let hits = AtomicU32::new(0);

for _ in 0..5 {
    let _ = hits.fetch_update(
        Ordering::Relaxed,
        Ordering::Relaxed,
        |n| (n < CAP).then(|| n + 1),
    );
}

assert_eq!(hits.load(Ordering::Relaxed), CAP);

The return value tells you what happened: Ok(prev) if your closure returned Some and the store went through, Err(prev) if it returned None — either way you get the previous value, so “did we hit the cap?” is just .is_err().

Two things to keep in mind. The closure can run more than once under contention (another thread changed the value between your load and the swap), so keep it pure — no side effects. And it takes two Orderings: the first for the successful store, the second for the loads; (Relaxed, Relaxed) is fine for counters.

Works on every Atomic* type, stable since Rust 1.45.

← Previous 273. remove_file — Deleting Something That Might Already Be Gone Next → 275. fetch_max — Track a High-Water Mark Without the Race