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

← Previous 310. f64::max — The Fold That Quietly Swallows NaN Next → 312. Duration::saturating_sub — Your Timeout Budget Ran Out, and the Subtraction Panicked