Loading pageā¦
Rust walkthroughs
Loading pageā¦
chrono::Duration::try_seconds for fallible duration construction?chrono::Duration::try_seconds provides a fallible constructor for creating durations from seconds that returns Option<Duration> instead of panicking or silently overflowing when the resulting duration would exceed the representable range. The standard Duration::seconds constructor either panics in debug mode or silently wraps in release mode when given values that would create an unrepresentable duration, but try_seconds gives you explicit control over error handling by returning None for out-of-range values. This is essential for production code where durations come from untrusted sourcesāconfiguration files, user input, network messagesābecause panicking on invalid input creates denial-of-service vulnerabilities, while silent overflow creates subtle timing bugs. The fallible API forces you to acknowledge the possibility of failure and handle it appropriately, whether by using default values, logging errors, or propagating failures up the call stack.
use chrono::Duration;
use std::thread;
fn main() {
// Duration::seconds() can panic or overflow
// Small values work fine
let small = Duration::seconds(60);
println!("One minute: {:?}", small);
// But what about extreme values?
// In debug mode, this panics:
// let huge = Duration::seconds(i64::MAX);
// thread 'main' panicked at 'overflow when adding duration to instant'
// In release mode, it wraps silently - even worse!
// The duration becomes a completely wrong value
// This is a problem for untrusted input:
let user_input: i64 = 1_000_000_000_000; // User wants 1 trillion seconds
// let duration = Duration::seconds(user_input); // Could panic!
// try_seconds returns None instead of panicking
match Duration::try_seconds(user_input) {
Some(d) => println!("Valid duration: {:?}", d),
None => println!("Duration out of range, using default"),
}
}try_seconds converts potential panics into Option<Duration>.
use chrono::Duration;
use std::i64;
fn main() {
// Duration internally uses nanoseconds as the unit
// With i64 storage, there are limits
// The maximum duration is approximately:
// i64::MAX nanoseconds ā 292 years
// Maximum in seconds: ~9.2 billion seconds
// try_seconds checks these bounds before construction
// These values are guaranteed to work:
let one_hour = Duration::try_seconds(3600);
println!("One hour: {:?}", one_hour);
let one_day = Duration::try_seconds(86400);
println!("One day: {:?}", one_day);
let one_year = Duration::try_seconds(365 * 86400);
println!("One year: {:?}", one_year);
// But extreme values fail:
let way_too_big = Duration::try_seconds(i64::MAX);
println!("i64::MAX seconds: {:?}", way_too_big); // None
let negative_huge = Duration::try_seconds(i64::MIN);
println!("i64::MIN seconds: {:?}", negative_huge); // None
// The actual limits depend on internal representation
// try_seconds handles the bounds checking for you
}try_seconds returns None when seconds would exceed the representable range.
use chrono::Duration;
fn main() {
// chrono provides try_* variants for all duration constructors
// try_seconds - seconds
let from_secs = Duration::try_seconds(100);
println!("From seconds: {:?}", from_secs);
// try_milliseconds - milliseconds
let from_millis = Duration::try_milliseconds(100_000);
println!("From milliseconds: {:?}", from_millis);
// try_microseconds - microseconds
let from_micros = Duration::try_microseconds(100_000_000);
println!("From microseconds: {:?}", from_micros);
// try_nanoseconds - nanoseconds
let from_nanos = Duration::try_nanoseconds(100_000_000_000);
println!("From nanoseconds: {:?}", from_nanos);
// All return Option<Duration>
// All handle overflow gracefully
// Compare with infallible versions that could panic:
// Duration::milliseconds(i64::MAX) // Panic or wrap!
// Corresponding try_ versions:
// Duration::try_milliseconds(i64::MAX) // Returns None
}All duration constructors have try_ variants for safe construction.
use chrono::Duration;
use std::collections::HashMap;
struct Config {
timeout_seconds: i64,
retry_delay_seconds: i64,
cache_ttl_seconds: i64,
}
fn load_config() -> Config {
// In real code, this might come from a file, environment, etc.
Config {
timeout_seconds: 30,
retry_delay_seconds: 5,
cache_ttl_seconds: 3600, // Could be set incorrectly
}
}
fn main() {
let config = load_config();
// Safe duration construction from config
let timeout = Duration::try_seconds(config.timeout_seconds)
.unwrap_or_else(|| {
eprintln!("Invalid timeout value, using default");
Duration::seconds(30) // Safe default
});
let retry_delay = Duration::try_seconds(config.retry_delay_seconds)
.unwrap_or(Duration::seconds(5));
let cache_ttl = Duration::try_seconds(config.cache_ttl_seconds)
.unwrap_or(Duration::seconds(300));
println!("Timeout: {:?}", timeout);
println!("Retry delay: {:?}", retry_delay);
println!("Cache TTL: {:?}", cache_ttl);
}Use try_seconds with defaults for configuration-derived values.
use chrono::Duration;
fn parse_duration_input(input: &str) -> Result<Duration, String> {
// Parse user input safely
let seconds: i64 = input.parse()
.map_err(|_| "Invalid number format".to_string())?;
// Check for negative values if needed
if seconds < 0 {
return Err("Duration cannot be negative".to_string());
}
// Try to construct duration
Duration::try_seconds(seconds)
.ok_or_else(|| "Duration value out of range".to_string())
}
fn main() {
// Test with various inputs
match parse_duration_input("3600") {
Ok(d) => println!("Valid: {:?}", d),
Err(e) => println!("Error: {}", e),
}
match parse_duration_input("999999999999999") {
Ok(d) => println!("Valid: {:?}", d),
Err(e) => println!("Error: {}", e), // "Duration value out of range"
}
match parse_duration_input("abc") {
Ok(d) => println!("Valid: {:?}", d),
Err(e) => println!("Error: {}", e), // "Invalid number format"
}
}try_seconds enables proper error handling for user-provided duration values.
use chrono::Duration;
fn main() {
// In release mode, Duration::seconds() can silently overflow
// This creates bugs that are hard to detect
// Example: Scheduling a future event
fn schedule_event_old(seconds: i64) -> Duration {
// This could wrap in release mode!
Duration::seconds(seconds)
}
// The wrapped duration might be negative or very small
// Leading to events firing immediately or at wrong times
// Safe version:
fn schedule_event_safe(seconds: i64) -> Option<Duration> {
Duration::try_seconds(seconds)
}
// Now caller must handle None case
// Example with realistic scenario:
let config_days = 3650; // 10 years in days (misconfig?)
let seconds = config_days * 86400; // This could overflow i64!
// Even the multiplication could overflow before reaching Duration
// So check the whole calculation chain
// Better approach: validate at each step
fn calculate_duration(days: i64) -> Option<Duration> {
// Check for reasonable bounds
if days < 0 || days > 365 * 100 { // Limit to 100 years
return None;
}
let seconds = days.checked_mul(86400)?;
Duration::try_seconds(seconds)
}
match calculate_duration(3650) {
Some(d) => println!("Duration: {:?}", d),
None => println!("Invalid duration"),
}
}Use try_seconds to prevent silent overflow in duration calculations.
use chrono::Duration as ChronoDuration;
use std::time::Duration as StdDuration;
fn main() {
// std::time::Duration has similar fallible constructors
// Infallible (can panic):
// StdDuration::from_secs(u64::MAX); // Would panic if too large
// Fallible (returns Result):
let std_duration = StdDuration::try_from_secs(u64::MAX);
println!("std duration: {:?}", std_duration); // Ok or Err
// chrono::Duration has try_seconds returning Option
let chrono_duration = ChronoDuration::try_seconds(i64::MAX);
println!("chrono duration: {:?}", chrono_duration); // Some or None
// Key differences:
// - std::time::Duration: Result<Duration, TryFromIntError>
// - chrono::Duration: Option<Duration>
// - std::time::Duration: u64 seconds (no negative)
// - chrono::Duration: i64 seconds (supports negative)
// chrono Duration supports negative durations
let negative = ChronoDuration::try_seconds(-100);
println!("Negative duration: {:?}", negative); // Some(Duration(-100s))
// std Duration cannot be negative
// StdDuration::try_from_secs(-1) // Would not compile (u64)
}Both offer fallible constructors; chrono::Duration supports negative values.
use chrono::{DateTime, Duration, Utc};
fn main() {
// A common use case: adding durations to dates
let now: DateTime<Utc> = Utc::now();
println!("Now: {}", now);
// Safe duration addition
fn add_seconds_safely(dt: DateTime<Utc>, seconds: i64) -> Option<DateTime<Utc>> {
let duration = Duration::try_seconds(seconds)?;
Some(dt + duration)
}
// With valid input
match add_seconds_safely(now, 3600) {
Some(future) => println!("One hour from now: {}", future),
None => println!("Invalid duration"),
}
// With invalid input
match add_seconds_safely(now, i64::MAX) {
Some(future) => println!("Far future: {}", future),
None => println!("Duration out of range"),
}
// This prevents panic when adding huge durations
}try_seconds prevents panics when adding durations to DateTime.
use chrono::Duration;
use std::collections::HashMap;
use std::time::Instant;
struct RateLimiter {
window_duration: Duration,
max_requests: u32,
requests: HashMap<String, Vec<Instant>>,
}
impl RateLimiter {
fn new(window_seconds: i64, max_requests: u32) -> Option<Self> {
// Validate window duration
let window_duration = Duration::try_seconds(window_seconds)?;
// Reasonable bounds check
if window_seconds < 1 || window_seconds > 86400 * 365 {
return None;
}
Some(Self {
window_duration,
max_requests,
requests: HashMap::new(),
})
}
fn is_allowed(&mut self, client_id: &str) -> bool {
let now = Instant::now();
let window_ago = now - std::time::Duration::from_secs(
self.window_duration.num_seconds() as u64
);
let client_requests = self.requests.entry(client_id.to_string()).or_default();
// Remove old requests outside window
client_requests.retain(|&t| t > window_ago);
if client_requests.len() >= self.max_requests as usize {
return false;
}
client_requests.push(now);
true
}
}
fn main() {
// Valid rate limiter
match RateLimiter::new(60, 100) {
Some(limiter) => println!("Rate limiter created"),
None => println!("Invalid configuration"),
}
// Invalid window (too large)
match RateLimiter::new(i64::MAX, 100) {
Some(limiter) => println!("Rate limiter created"),
None => println!("Invalid configuration - window too large"),
}
// Invalid window (zero)
match RateLimiter::new(0, 100) {
Some(limiter) => println!("Rate limiter created"),
None => println!("Invalid configuration - window too small"),
}
}Use try_seconds to validate configuration bounds in constructors.
use chrono::Duration;
fn main() {
// try_seconds returns Option, which integrates with ? operator
fn calculate_total_time(parts: &[i64]) -> Option<Duration> {
let mut total = Duration::zero();
for &seconds in parts {
// This propagates None if any part fails
let part_duration = Duration::try_seconds(seconds)?;
total = total.checked_add(&part_duration)?;
}
Some(total)
}
// Valid parts
match calculate_total_time(&[60, 120, 180]) {
Some(d) => println!("Total: {:?}", d),
None => println!("Calculation failed"),
}
// Invalid part
match calculate_total_time(&[60, i64::MAX, 180]) {
Some(d) => println!("Total: {:?}", d),
None => println!("Calculation failed - invalid part"),
}
// The ? operator makes error propagation clean
// No explicit match statements needed
}Option return type works well with ? for clean error propagation.
Constructor comparison:
| Method | Return Type | Behavior on Overflow |
|--------|-------------|---------------------|
| Duration::seconds(i64) | Duration | Panic (debug) or wrap (release) |
| Duration::try_seconds(i64) | Option<Duration> | Returns None |
| Duration::milliseconds(i64) | Duration | Panic or wrap |
| Duration::try_milliseconds(i64) | Option<Duration> | Returns None |
When to use each:
| Situation | Recommended Constructor |
|-----------|------------------------|
| Hard-coded constants | Duration::seconds() (safe) |
| Configuration values | Duration::try_seconds() |
| User input | Duration::try_seconds() |
| Network/API input | Duration::try_seconds() |
| Calculated values | Duration::try_seconds() |
| Arithmetic results | Duration::try_seconds() |
Key insight: chrono::Duration::try_seconds exists because durations derived from external sources cannot be trusted to fit within the representable range, and the consequences of overflow vary from subtle bugs (silent wrapping) to catastrophic failures (panics in production). The try_ prefix signals that failure is possible and must be handled. This is a pattern borrowed from Rust's broader approach to fallibility: try_from, try_into, try_get, etc. all represent operations that might fail. For durations specifically, the risk is that a single malformed configuration value could crash a service or cause incorrect scheduling, and both outcomes are unacceptable in production systems. By returning Option<Duration>, try_seconds forces the caller to decide: use a default, log an error, fail the operation, or propagate the failure. This explicit error handling is safer than hoping the value fits, because hope is not a strategy for reliability. The pattern extends across the chrono API: every unit constructor has a try_ variant, making it possible to build robust systems where all duration values are validated at their point of entry.