Nonzero

#185 Jun 2026

185. Range<NonZeroU32> — Iterate NonZero Integers Without Re-Wrapping Every Step

NonZeroU32 keeps Option<Id> at 4 bytes — but until Rust 1.96 you couldn’t iterate lo..hi over them. You’d drop back to u32 and re-wrap every step.

NonZeroU32 and friends are great for indices and IDs because Option<NonZeroU32> fits the niche and stays 4 bytes wide. The catch: Range<NonZeroU32> wasn’t an iterator. The moment you wanted to walk a range of IDs, you fell back to plain u32 and unwrapped your way back in:

1
2
3
4
5
6
7
8
9
use std::num::NonZeroU32;

let lo = NonZeroU32::new(1).unwrap();
let hi = NonZeroU32::new(5).unwrap();

// Pre-1.96: lift, iterate plain ints, re-wrap each step.
let ids: Vec<NonZeroU32> = (lo.get()..hi.get())
    .map(|n| NonZeroU32::new(n).unwrap())
    .collect();

Three problems: the Range drops the invariant, every step pays for an unwrap, and a future refactor that changes the bound type silently swaps your iterator out from under you.

Rust 1.96 stabilized Step for NonZero integers (PR #127534). Now the range itself is an iterator that yields NonZeroU32:

1
2
3
4
5
6
7
8
use std::num::NonZeroU32;

let lo = NonZeroU32::new(1).unwrap();
let hi = NonZeroU32::new(5).unwrap();

let ids: Vec<NonZeroU32> = (lo..hi).collect();
assert_eq!(ids.len(), 4);
assert_eq!(ids[0].get(), 1);

It works for the inclusive form too, so you can sweep the whole representable range without overflow gymnastics:

1
2
3
4
5
6
use std::num::NonZeroU8;

let total: u32 = (NonZeroU8::MIN..=NonZeroU8::MAX)
    .map(|n| n.get() as u32)
    .sum();
assert_eq!(total, 32_640); // 1 + 2 + ... + 255

If you keep your IDs in a NonZeroU32 newtype to shrink Option, the iteration story now matches: the range yields the right type the whole way through, no per-step unwrap, no invariant laundering through u32.

#173 May 2026

173. NonZeroU32 and Friends — Encode an Invariant and Shrink Option for Free

Option<u32> is 8 bytes. Option<NonZeroU32> is 4 bytes. Same information, half the size — and the compiler enforces “this can’t be zero” for you.

The problem

You have an ID, a port number, or a child-process exit code. It’s logically a u32, but 0 is meaningless or sentinel-only. So you reach for Option<u32>:

1
struct Handle { id: Option<u32> }

That’s now 8 bytes: 4 for the u32 and 4 more for the discriminant telling you which variant you’re in. Even worse, “no ID yet” and “ID is zero” are two distinct states the compiler can’t tell apart for you.

The fix: NonZero*

std::num::NonZeroU32 (and its siblings NonZeroI64, NonZeroUsize, etc.) is a u32 that’s guaranteed at the type level to never be zero. Constructors return Option so you can’t accidentally build an invalid one:

1
2
3
4
5
use std::num::NonZeroU32;

let port = NonZeroU32::new(8080).expect("non-zero");
assert_eq!(port.get(), 8080);
assert!(NonZeroU32::new(0).is_none());

Because zero is now “impossible,” the compiler reuses that bit pattern as the None discriminant — this is the niche optimization:

1
2
3
4
5
6
7
use std::mem::size_of;
use std::num::NonZeroU32;

assert_eq!(size_of::<u32>(), 4);
assert_eq!(size_of::<Option<u32>>(), 8);          // discriminant + payload
assert_eq!(size_of::<NonZeroU32>(), 4);
assert_eq!(size_of::<Option<NonZeroU32>>(), 4);   // free!

Wrap it in a newtype

The real win is making invalid states unrepresentable. Stop passing raw u32 around and use a newtype:

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

#[derive(Copy, Clone, Debug)]
struct UserId(NonZeroU32);

impl UserId {
    fn new(raw: u32) -> Option<Self> {
        NonZeroU32::new(raw).map(UserId)
    }
    fn get(self) -> u32 {
        self.0.get()
    }
}

let alice = UserId::new(42).unwrap();
assert!(UserId::new(0).is_none());
assert_eq!(alice.get(), 42);

Now Option<UserId> is 4 bytes, Vec<Option<UserId>> is half the memory it would otherwise be, and “this u32 is a real user id, not a placeholder” is checked at construction, not at every call site.

When to reach for it

Any u32/u64/usize where zero is invalid: database row IDs, file descriptors, generation counters, capacity-like values, lengths that must be at least one. The generic NonZero<T> form also exists for cleaner code: NonZero<u32> reads the same and works in const contexts since Rust 1.79.