Num

#167 May 2026

167. Saturating<T> — Stop Calling .saturating_add() Everywhere

When every step in a loop or formula could overflow, calling .saturating_add() and .saturating_sub() on each one turns one line of math into a paragraph.

std::num::Saturating<T> is a tuple newtype wrapper that overloads the normal operators (+, -, *, +=, …) to use saturating semantics — operations clamp to the type’s MIN or MAX instead of wrapping or panicking. You write a + b, not a.saturating_add(b).

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

let hp: Saturating<u8> = Saturating(250);
let heal = Saturating(20u8);
let damage = Saturating(255u8);

// Pins at 255 instead of wrapping or panicking.
let healed = hp + heal;
assert_eq!(healed.0, 255);

// Pins at 0 instead of underflowing.
let dead = hp - damage;
assert_eq!(dead.0, 0);

The unwrapped equivalent works, but every operator turns into a method call:

1
2
3
4
5
let hp: u8 = 250;
let healed = hp.saturating_add(20);
let dead = hp.saturating_sub(255);
assert_eq!(healed, 255);
assert_eq!(dead, 0);

The wrapper really pays off inside a formula or an iterator chain, where you’d otherwise be wrapping each binary op:

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

let damages = [Saturating(200u8), Saturating(100), Saturating(50)];
let total: Saturating<u8> = damages.iter().copied().sum(); // sticks at 255
assert_eq!(total.0, 255);

There’s a matching std::num::Wrapping<T> when you actually want cyclical math (hashes, CRCs, monotonic counters that should roll over), and both wrappers implement From, Default, Sum, and Product, so you can drop them into structs and iterator chains without ceremony.

Reach for Saturating<T> whenever a value has a logical floor or ceiling — health bars, progress percentages, retry budgets — and overflow should pin to the edge instead of panicking in debug or silently wrapping in release.