#310 Aug 10, 2026

310. f64::max — The Fold That Quietly Swallows NaN

f64::NAN.max(3.0) is 3.0. A running max over data with a NaN in it returns a perfectly clean-looking number — and the is_nan check downstream swears nothing went wrong.

f64::max and f64::min follow IEEE 754’s maxNum/minNum semantics: if one operand is NaN, they return the other one. So NaN doesn’t poison the result the way it does with + or * — it silently drops out:

1
2
3
4
5
6
7
8
assert_eq!(f64::NAN.max(3.0), 3.0);
assert_eq!(3.0_f64.max(f64::NAN), 3.0);

let v = [2.5, f64::NAN, 3.1];
let max = v.iter().copied().fold(f64::MIN, f64::max);

assert_eq!(max, 3.1);   // NaN vanished
assert!(!max.is_nan()); // your check never fires

Sometimes that’s exactly what you want — treat NaN as “no data” and take the max of what’s left. But if a NaN in the input means the computation upstream went wrong, checking the result tells you nothing: the fold already laundered it.

The stable way to make NaN surface is total_cmp (the same total ordering from bite 101). In IEEE total order, positive NaN sorts above +inf, so a NaN in the data becomes the maximum instead of disappearing:

1
2
3
4
5
let strict = v.iter().copied()
    .max_by(|a, b| a.total_cmp(b))
    .unwrap();

assert!(strict.is_nan()); // NaN surfaces

(There are NaN-propagating f64::maximum / f64::minimum methods in the works, but they’re still unstable — total_cmp is the fix on stable.)

Like the subnormals in bite 309, this is a case where the value you’re inspecting passes every check while the data behind it lies. Decide explicitly: NaN-ignoring (f64::max) or NaN-surfacing (max_by(total_cmp)) — just don’t let the default decide for you.

← Previous 309. classify — x > 0.0 Passed, Then 1.0 / x Returned inf Next → 311. Duration::try_from_secs_f64 — Your Backoff Math Went Negative, and from_secs_f64 Panicked