#312 Aug 11, 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.

← Previous 311. Duration::try_from_secs_f64 — Your Backoff Math Went Negative, and from_secs_f64 Panicked Next → 313. div_duration_f64 — Your Progress Bar Sat at 0% Because as_secs() Truncated First