Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-678 · Case file with fixtures · Case 650 of 694 · Runtime evidence

Duration Subtraction Panics on Underflow

Duration is non-negative and its subtraction operator requires a representable result. Use checked_sub for validation or saturating_sub only when zero is the honest floor.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Operator subtraction assumes a proven left-greater-than-or-equal invariant on a non-negative time span type.
First discriminating check
Select checked subtraction for validation or saturating subtraction only when zero is an honest documented domain floor.

std::time::Duration represents a non-negative span. Using the subtraction operator when the right side is larger cannot produce a valid Duration and panics. The failing fixture calculates two seconds minus five and triggers that contract.

Duration has no negative values

A Duration stores seconds and fractional nanoseconds for an unsigned magnitude. It is useful for timeouts, elapsed spans, and intervals. It is not a signed difference type.

The subtraction operator is convenient when the invariant left >= right is already proven. If that invariant can be false because of input, races, rounding, or clock comparisons, the operator turns a domain case into a panic.

I make uncertainty visible with checked_sub. The repaired fixture receives None, which the caller can convert into an error or another policy.

Checked and saturating answers mean different things

saturating_sub returns zero on underflow. This is useful for remaining-budget calculations where an overdue task honestly has zero time left.

It is dangerous when a negative difference indicates reordered events, corrupt data, or an accounting error. Saturation erases how far and why the invariant failed. Checked subtraction preserves the branch.

I use operator subtraction for proven preconditions, checked subtraction for validation, and saturating subtraction only for a named floor-at-zero domain rule.

Deadlines are often better than repeatedly shrinking durations

A timeout budget can be represented as a monotonic deadline. Each operation computes remaining time from the current instant. This avoids subtracting several measured steps from an original duration and accumulating rounding or bookkeeping errors.

The deadline comparison still needs correct operand order and checked behaviour. If it has passed, remaining duration is zero or a timeout outcome according to the API.

Wall-clock timestamps should not measure elapsed process time because system time can move. Instant is designed for monotonic measurements, subject to its platform guarantees.

Units and conversions can hide underflow

Parsing milliseconds into Duration and subtracting seconds converted elsewhere invites unit mistakes. I use names carrying units at raw numeric boundaries and convert to Duration once.

Floating-point constructors and conversions require policies for negative, NaN, infinite, and precision-losing values. A Duration being type-safe after construction does not validate the source meaning.

When serializing, I define resolution and maximum size. Nanoseconds may overflow a smaller integer even though the Duration itself is valid. Checked conversions keep this separate from subtraction.

Concurrency makes stale budgets common

Two workers can observe the same remaining budget and both consume it. A local Duration is only a snapshot, not a reservation. Shared rate limits need synchronization or an atomic domain protocol.

Similarly, a retry loop may spend more time than one measured operation reports because scheduling, logging, and backoff also consume time. I measure against one deadline rather than manually subtract every estimated cost.

Tests cover equal spans, one-nanosecond underflow, normal subtraction, expired deadlines, and very large durations. They assert the selected policy: None, zero, or panic for an internal violated invariant.

Preserve the reason for zero

A remaining duration of zero can mean that a deadline has just arrived, passed long ago, or was clamped after invalid input. If those cases affect observability or retry policy, I retain a separate enum such as Remaining, Expired, or InvalidOrder instead of passing only the Duration onward. A numeric floor is useful for a timer API, while the richer state remains useful to the application.

Backoff calculations have the same issue at the upper end. Repeated multiplication can overflow even when subtraction is checked. I cap with a documented maximum and use checked or saturating multiplication according to whether reaching the cap is normal. Jitter is then applied inside the valid bounded range.

My duration checklist

  • Can the right span be larger than the left?
  • Should underflow be an error, zero, or impossible invariant?
  • Would a monotonic deadline express the budget better?
  • Are raw numeric units named and converted once?
  • Can serialization narrow a valid duration?
  • Is a local remaining value mistaken for a concurrent reservation?
  • Are wall-clock and monotonic time used for their correct roles?
  • Do tests include equality and one-unit underflow?

The core principle is that an unsigned time span cannot encode negative meaning. Duration subtraction therefore needs a policy whenever ordering is uncertain. I choose checked evidence or deliberate saturation rather than let ordinary external timing become a surprise panic.