RFA-185 · Case file with fixtures · Case 157 of 694 · Runtime evidence
Why Duration::new Normalizes Excess Nanoseconds
Duration stores a canonical value, not the original seconds-and-nanoseconds spelling. Its constructor carries excess nanoseconds into seconds; validate first when non-canonical external fields must be rejected.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- The constructor normalizes excess nanoseconds by carrying whole billions into the seconds component and panics only if that carry overflows.
- First discriminating check
- Inspect as_secs and subsec_nanos separately, then decide whether external input should be normalized or rejected as non-canonical.
Duration::new(seconds, nanoseconds) accepts two components, but a Duration represents one normalized amount of time. It does not remember the original spelling of those components.
The failing program constructs one second plus 1.5 billion nanoseconds. The resulting whole-seconds component is 2, not 1.
Excess nanoseconds carry into seconds
Duration::new treats one billion nanoseconds as one second. When nanos is at least one billion, the constructor carries whole units into the seconds field.
The example becomes:
input: 1 second + 1,500,000,000 nanoseconds
normalized: 2 seconds + 500,000,000 nanoseconds
The value is correctly 2.5 seconds. What disappears is the fact that the caller supplied a non-canonical nanosecond component.
subsec_nanos therefore always reports the fractional remainder below one billion, not the raw constructor argument.
Normalization and validation serve different contracts
Normalization is useful when components come from arithmetic. It gives equivalent amounts one representation and makes equality straightforward.
At a protocol boundary, the schema may require nanos to be between zero and 999,999,999. Accepting and normalizing 1.5 billion could hide malformed or incompatible data.
The repaired fixture checks the nanosecond range before constructing the duration. That preserves the protocol's canonical-input rule.
I decide deliberately:
- normalize when several equivalent component spellings are accepted;
- reject when the input format requires canonical fields;
- preserve the raw fields separately when their original form matters for auditing.
Overflow happens during the carry
The constructor panics if adding carried seconds would overflow the duration's seconds counter. A range check on nanos avoids carry in a canonical protocol, but arbitrary arithmetic may still approach Duration::MAX.
For calculations where overflow is a normal possibility, checked operations such as Duration::checked_add return Option rather than panicking. Saturating operations express yet another policy.
I do not choose saturating arithmetic only to remove errors. Turning an excessive timeout into the maximum duration can effectively mean “never,” which may be dangerous.
Units should appear in names and types
Many duration bugs happen before construction: a millisecond count is passed as seconds, or nanoseconds are narrowed to u32 too early.
I use conversion constructors such as from_millis when the input already has one unit. For structured seconds/nanos input, I validate each field and checked-convert wider integers before calling new.
Variables called only timeout or value hide unit mistakes. timeout_ms, seconds, and subsecond_nanos make review easier even when a dedicated type is not practical.
Duration is not a timestamp
Duration is non-negative and describes an amount of time. It does not contain a timezone, clock source, calendar meaning, or wall-clock instant.
A normalized duration cannot repair confusion between elapsed monotonic time and civil time. Adding “one day” as 86,400 seconds is not always the same as moving to the next local calendar date.
I keep duration parsing close to the domain boundary and convert to clock-specific types only where their semantics are known.
Floating-point constructors need another policy
Seconds represented as floating point introduce NaN, infinity, negative values, and rounding. Even when a constructor accepts a finite value, round-tripping textual decimals may not reproduce the original spelling.
For exact protocols I prefer integer units or structured integer components. For measurement and approximate configuration, floating-point conversion can be appropriate with explicit range checks.
Again, the key is whether I am preserving an amount, a representation, or both.
My duration boundary checklist
When a time value changes shape or overflows, I ask:
- What unit does each input field use?
- Must subsecond fields already be canonical?
- Should equivalent non-canonical inputs normalize or fail?
- Can carrying or later arithmetic exceed
Duration::MAX? - Is checked, saturating, or panicking arithmetic the correct policy?
- Does the system need an elapsed duration, timestamp, or calendar interval?
- Must the original input representation remain auditable?
The core principle is that constructors can canonicalize information. Duration::new preserves the total amount while changing its component representation. If malformed component structure matters, I validate before normalization removes the evidence.