#316 Aug 2026

316. checked_duration_since — Your Deadline Passed, and Instant Subtraction Said Zero

This morning’s bite 315 said Instant has no Result to handle. True — but its subtraction hides a different trap: past a deadline, deadline - now is silently 0, and your retry loop spins forever with a zero timeout.

Since Rust 1.60, subtracting Instants in the wrong order doesn’t panic — it saturates to zero:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::time::{Duration, Instant};

let earlier = Instant::now();
let later = earlier + Duration::from_secs(5);

// right order: the real gap
assert_eq!(later.duration_since(earlier), Duration::from_secs(5));

// wrong order: silently zero — no panic, no Err
assert_eq!(earlier.duration_since(later), Duration::ZERO);
assert_eq!(earlier - later, Duration::ZERO);

That’s kind to platform clock bugs, but cruel to deadline code. The classic pattern computes the remaining budget each iteration:

1
2
3
4
5
let remaining = deadline - Instant::now();
socket.set_read_timeout(Some(remaining)); // 💣 once the deadline
                                          // passes, remaining is 0
                                          // — which many APIs read
                                          // as "no timeout at all"

Zero never looks like “we’re done” — it looks like “no time elapsed”. Worse, set_read_timeout(Some(Duration::ZERO)) is an error on std sockets, and other APIs treat zero as infinite wait.

checked_duration_since tells the truth

None means the deadline is behind you — an unmissable, type-checked “time’s up”:

1
2
3
4
5
6
7
use std::time::Instant;

fn remaining(deadline: Instant) -> Option<std::time::Duration> {
    deadline.checked_duration_since(Instant::now())
}

// Some(budget) → keep waiting; None → deadline passed, bail
1
2
3
4
5
let earlier = Instant::now();
let later = earlier + Duration::from_secs(5);

assert_eq!(later.checked_duration_since(earlier), Some(Duration::from_secs(5)));
assert_eq!(earlier.checked_duration_since(later), None);

And when clamping to zero is what you want, say it explicitly with saturating_duration_since — same behavior as the bare subtraction, but the next reader knows it’s deliberate:

1
assert_eq!(earlier.saturating_duration_since(later), Duration::ZERO);

Same trio as integer arithmetic — bare op, checked_*, saturating_* — just on the clock. Pick the one that says what you mean.

#315 Aug 2026

315. SystemTime::duration_since — NTP Set the Clock Back, and elapsed().unwrap() Panicked

You timed a job with SystemTime and it ran fine for weeks — until NTP stepped the clock backwards and that innocent .elapsed().unwrap() took the service down.

SystemTime is the wall clock. It can jump backwards at any moment: NTP corrections, a manual clock change, a VM resuming from snapshot. That’s why every subtraction on it returns a Result — the “later” time can end up earlier:

1
2
3
4
5
6
use std::time::SystemTime;

let start = SystemTime::now();
do_work();
let taken = start.elapsed().unwrap(); // 💣 panics if
                                      // the clock stepped back

The Err isn’t noise to unwrap away — it’s the whole point of the API. It even tells you how far backwards the clock went:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::time::{Duration, SystemTime};

let earlier = SystemTime::now();
let later = earlier + Duration::from_secs(5);

// forward: Ok(gap)
assert_eq!(
    later.duration_since(earlier).unwrap(),
    Duration::from_secs(5)
);

// backwards: Err, carrying the gap
let err = earlier.duration_since(later).unwrap_err();
assert_eq!(err.duration(), Duration::from_secs(5));

Measuring time? Use Instant

If you’re timing how long something took, you don’t want the wall clock at all. Instant is the monotonic clock — the OS guarantees it never goes backwards, so there’s no Result to handle:

1
2
3
4
5
use std::time::Instant;

let start = Instant::now();
do_work();
let taken = start.elapsed(); // Duration, always valid

The rule of thumb

Instant for durations, SystemTime for timestamps. The one thing Instant can’t do is tell you when — it’s opaque, with no relation to any calendar. When you need an actual timestamp (log lines, cache expiry dates, file metadata), that’s SystemTime’s job:

1
2
3
4
5
6
use std::time::{SystemTime, UNIX_EPOCH};

let unix_secs = SystemTime::now()
    .duration_since(UNIX_EPOCH)
    .expect("clock before 1970")
    .as_secs();

And if a backwards step is survivable in your context, unwrap_or_default() turns it into a zero duration instead of a panic — the same “clamp at zero” move as saturating_sub in bite 312.

#314 Aug 2026

314. Duration::mul_f64 — timeout * 1.5 Doesn't Compile, So You Round-Tripped Through Floats

Duration * 2 compiles, Duration * 1.5 doesn’t — and the usual workaround tears the duration apart into an f64 just to build it back up again.

Duration implements Mul<u32>, so doubling a timeout is easy. But backoff factors are rarely whole numbers — the classic multiplier is 1.5 or 2.0-with-jitter, and that’s where the type checker stops you:

1
2
3
4
5
6
7
use std::time::Duration;

let delay = Duration::from_millis(200);

let doubled = delay * 2;        // fine: Mul<u32>
// let next = delay * 1.5;      // error: cannot multiply
//                              // `Duration` by `{float}`

The workaround everyone reaches for is the float round-trip:

1
let next = Duration::from_secs_f64(delay.as_secs_f64() * 1.5);

It works, but it’s noisy, and it funnels a perfectly good Duration through a lossy f64 in both directions.

Multiply the duration directly

mul_f64 (and div_f64) have been on Duration since Rust 1.38 — they do the scaling in one call and keep nanosecond precision:

1
2
3
4
5
6
use std::time::Duration;

let delay = Duration::from_millis(200);

assert_eq!(delay.mul_f64(1.5), Duration::from_millis(300));
assert_eq!(delay.div_f64(2.0), Duration::from_millis(100));

Which makes an exponential backoff loop read exactly like the algorithm:

1
2
3
4
5
6
let mut delay = Duration::from_millis(100);
for _ in 0..4 {
    delay = delay.mul_f64(1.5).min(Duration::from_secs(2));
}
// 100ms → 150ms → 225ms → 337.5ms → 506.25ms
assert_eq!(delay, Duration::new(0, 506_250_000));

One caveat carried over from float land: mul_f64 panics if the factor produces a negative, non-finite, or overflowing result — the same failure mode as from_secs_f64 in bite 311. If your factor comes from untrusted math (jitter that can dip negative), clamp it first or go through Duration::try_from_secs_f64.

That closes out the week’s Duration tour: 311 built one from a float safely, 312 subtracted without panicking, 313 divided two of them — and today’s bite scales one without leaving the type.

#313 Aug 2026

313. div_duration_f64 — Your Progress Bar Sat at 0% Because as_secs() Truncated First

Dividing two Durations with as_secs() is integer division — 1.5s of 60s truncates to 0, and your progress bar never moves.

The obvious way to compute “how far along are we” divides seconds by seconds:

1
2
3
4
5
6
7
8
use std::time::Duration;

let elapsed = Duration::from_millis(1500);
let total = Duration::from_secs(60);

// 1 / 60 == 0 — integer division truncates
let frac = elapsed.as_secs() / total.as_secs();
assert_eq!(frac, 0);

as_secs() returns u64, so the sub-second part is gone before the division even runs, and the quotient truncates to zero on top of that. Switching to as_millis() just moves the truncation point; switching both sides to as_secs_f64() works but says nothing about intent.

Divide the durations directly

div_duration_f64 (stable since 1.80) divides one Duration by another and gives back the ratio as a float, sub-second precision included:

1
2
3
4
5
6
7
use std::time::Duration;

let elapsed = Duration::from_millis(1500);
let total = Duration::from_secs(60);

let frac = elapsed.div_duration_f64(total);
assert_eq!(frac, 0.025);

It reads in the same direction as the math: elapsed.div_duration_f64(total) is elapsed ÷ total. The same shape works for any “how many times does this fit” question — like a benchmark speedup:

1
2
3
4
let old = Duration::from_millis(750);
let new = Duration::from_millis(250);

assert_eq!(old.div_duration_f64(new), 3.0);

Two things worth knowing: there’s a div_duration_f32 twin if you’re feeding a graphics API, and dividing by Duration::ZERO follows float semantics — you get inf, not a panic. That’s one less edge case than the integer route, where total.as_secs() being zero would have crashed the division outright.

Like bite 311 and bite 312, the theme is the same: Duration already has a method for the math you’re about to do by hand — and the hand-rolled version is where the bugs live.

#312 Aug 2026

312. Duration::saturating_sub — Your Timeout Budget Ran Out, and the Subtraction Panicked

Duration - Duration panics on underflow — and budget - elapsed underflows the moment a slow operation blows the budget.

A classic timeout-budget loop: give the whole request 200 ms, subtract what each step used, hand the rest to the next step. The subtraction is the trap:

1
2
3
4
5
6
7
use std::time::Duration;

let budget = Duration::from_millis(200);
let elapsed = Duration::from_millis(250);

// panics: overflow when subtracting durations
let remaining = budget - elapsed;

Duration is unsigned — there is no negative duration — so the moment elapsed exceeds budget, Sub has nothing valid to return and panics. It works fine in tests where every step is fast, then crashes in production the first time a step stalls.

The fallible and saturating versions

checked_sub returns an Option, which makes “budget exhausted” an explicit case instead of a crash:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::time::Duration;

let budget = Duration::from_millis(200);
let slow = Duration::from_millis(250);
let fast = Duration::from_millis(50);

assert_eq!(budget.checked_sub(slow), None);
assert_eq!(
    budget.checked_sub(fast),
    Some(Duration::from_millis(150))
);

If “no time left” should just mean zero — say, the value goes straight into recv_timeout from bite 283saturating_sub clamps instead:

1
2
let remaining = budget.saturating_sub(slow);
assert_eq!(remaining, Duration::ZERO);

The same family exists for the other direction: checked_add / saturating_add for accumulating durations, and checked_mul / checked_div for scaling them.

This is the integer twin of this morning’s bite 311: there, float math produced a negative that panicked at the Duration conversion; here, Duration math itself hits the floor. Either way, the fix is the same — reach for the method that returns a value you can handle instead of the operator that panics.

#311 Aug 2026

311. Duration::try_from_secs_f64 — Your Backoff Math Went Negative, and from_secs_f64 Panicked

Duration::from_secs_f64 panics on negative, NaN, or overflowing input — exactly the values float math produces when a jittered backoff dips below zero.

Computing a delay in float land is convenient: multiply a base by a factor, subtract some jitter, done. But the conversion back to Duration is a trap:

1
2
3
4
5
6
7
8
9
use std::time::Duration;

let base: f64 = 0.2;
let jitter: f64 = 0.3;
let secs = base - jitter; // -0.1

// panics: can not convert float seconds
// to Duration: value is negative
let delay = Duration::from_secs_f64(secs);

The panic conditions are anything that isn’t representable: negative values, NaN, and anything too large for Duration. All three are one arithmetic slip away — a subtraction that dips below zero, a 0.0 / 0.0 hiding in a rate calculation, an overflowing powi on the retry counter.

The fallible version hands you a Result

Duration::try_from_secs_f64 (stable since 1.66) does the same conversion but returns Err instead of panicking:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::time::Duration;

let ok = Duration::try_from_secs_f64(1.5);
assert_eq!(ok.unwrap(), Duration::from_millis(1500));

assert!(Duration::try_from_secs_f64(-0.1).is_err());
assert!(Duration::try_from_secs_f64(f64::NAN).is_err());
assert!(
    Duration::try_from_secs_f64(f64::INFINITY).is_err()
);

For backoff code, the natural move is to clamp the failure to something sane instead of crashing the retry loop:

1
2
3
4
let secs = -0.1_f64; // jitter took us negative
let delay = Duration::try_from_secs_f64(secs)
    .unwrap_or(Duration::ZERO);
assert_eq!(delay, Duration::ZERO);

There’s a try_from_secs_f32 twin for f32 inputs.

Like classify in bite 309, the point is that the float you’re holding can be quietly unrepresentable — and the moment of conversion is where that surfaces. try_from_secs_f64 lets it surface as a value you can handle, not a panic in the middle of your retry loop.

#310 Aug 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.

#309 Aug 2026

309. classify — x > 0.0 Passed, Then 1.0 / x Returned inf

Between zero and the smallest “real” float lives a twilight zone of subnormals — values that pass your > 0.0 guard but blow up the moment you divide by them.

f64::MIN_POSITIVE (~2.2e-308) is the smallest normal float. Below it, floats go subnormal: the exponent is maxed out, so the mantissa gives up leading bits and precision degrades. They’re not zero, but they don’t behave like the floats you know:

1
2
3
4
5
6
7
8
let x = 1e-310_f64; // fell out of some computation

// your guard passes...
assert!(x > 0.0);
assert!(x.is_finite());

// ...but the reciprocal overflows
assert!((1.0 / x).is_infinite());

is_normal is the one-line fix — it’s true only for regular floats, and false for zero, subnormals, infinities, and NaN:

1
2
3
assert!(!x.is_normal());
assert!(x.is_subnormal());
assert!(1.0_f64.is_normal());

When you need to know which weird case you got, classify turns the whole is_nan / is_infinite / == 0.0 ladder into one exhaustive match:

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

match x.classify() {
    FpCategory::Normal => { /* safe to invert */ }
    FpCategory::Zero | FpCategory::Subnormal => { /* underflow */ }
    FpCategory::Infinite | FpCategory::Nan => { /* bad input */ }
}
assert_eq!(x.classify(), FpCategory::Subnormal);

The match is exhaustive, so unlike the boolean ladder, the compiler makes sure you handled every category — including the one you forgot exists.

If a division’s denominator came from data instead of a literal, denom.is_normal() is the guard you meant when you wrote denom != 0.0.

#308 Aug 2026

308. sin_cos — One Angle, Both Halves, No Swap Bug

Every rotation, polar conversion, and circle you draw needs sin and cos of the same angle — and every codebase has one spot where x got the sin and y got the cos.

Two separate calls means two chances to put the results in the wrong slot, and the compiler can’t help — both are f64. sin_cos returns the pair as a tuple, so the destructure names them once and the math below reads like math:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let theta = 30.0_f64.to_radians(); // bite 307
let r = 2.0;

// two calls, two chances to swap them
let x = r * theta.cos();
let y = r * theta.sin();

// one call, one destructure
let (sin, cos) = theta.sin_cos();
let (x, y) = (r * cos, r * sin);
assert!((y - 1.0).abs() < 1e-15); // r·sin 30° = 1

One thing to burn in: the tuple is (sin, cos) — sin first, matching the method name, even though (x, y) order would put cos first. Destructure as let (s, c) = … and the names keep you honest.

The classic customer is a 2D rotation, where the same sin and cos appear four times:

1
2
3
4
5
6
7
fn rotate((x, y): (f64, f64), theta: f64) -> (f64, f64) {
    let (s, c) = theta.sin_cos();
    (x * c - y * s, x * s + y * c)
}

let (x, y) = rotate((1.0, 0.0), 90.0_f64.to_radians());
assert!(x.abs() < 1e-15 && (y - 1.0).abs() < 1e-15);

Without sin_cos that function either calls sin and cos twice each, or you write the two temporaries by hand — which is exactly where the swap sneaks in.

Don’t reach for it as a speed hack: std is free to implement it as the two calls you’d have written, and often does. The win is intent — one angle in, both halves out, and bite 300’s atan2 will happily turn the pair back into the angle:

1
2
let (s, c) = 1.0_f64.sin_cos();
assert!((s.atan2(c) - 1.0).abs() < 1e-15);

If a sin and a cos of the same angle sit within three lines of each other, they want to be one sin_cos.

#307 Aug 2026

307. to_degrees / to_radians — Stop Hand-Typing 180.0 / PI

sin wants radians, your users want degrees, and somewhere in your codebase a hand-typed * 180.0 / PI is waiting to be fat-fingered. The conversion has been a method on floats since Rust 1.0.

Every trig function in std speaks radians. Every protractor, compass heading, and UI slider speaks degrees. So this line gets written over and over:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::f64::consts::PI;

let heading: f64 = 90.0;

// hand-rolled
let rad = heading * PI / 180.0;

// stdlib
let rad = heading.to_radians();
assert!((rad - PI / 2.0).abs() < 1e-15);

Same in the other direction — bite 300’s atan2 hands you radians, and to_degrees turns them back into something a human can read:

1
2
let angle = 1.0_f64.atan2(1.0); // 45° as radians
assert!((angle.to_degrees() - 45.0).abs() < 1e-12);

The readability win is obvious: no magic constant to mistype (57.2958, 0.0174533, and their truncated cousins show up in real codebases), and the intent is in the method name instead of a comment.

There’s a correctness win too. to_degrees multiplies by one precomputed constant — 180.0 / PI rounded once at full precision. The hand-rolled version does two operations, and the intermediate x * 180.0 can overflow even when the final result is representable:

1
2
3
4
let big = f64::MAX / 100.0;

assert!((big * 180.0 / PI).is_infinite()); // x * 180 blew up
assert!(big.to_degrees().is_finite());     // one multiply, no overflow

The everyday use is keeping unit confusion out of trig calls — convert at the boundary, and everything inside stays in radians:

1
2
let slope = 30.0_f64.to_radians().sin();
assert!((slope - 0.5).abs() < 1e-15);

If a PI / 180.0 appears anywhere outside a constants module, it wants to be a to_radians.