ogham/exa · error
Offset out of range
Error message
Offset out of range
What it means
This is a Rust panic from expect() in src/output/time.rs:130 inside full_zoned(). The datetime crate's Offset::of_seconds() returns Option<Offset>: it is None when the offset in seconds cannot be represented as a ±24-hour UTC offset (or when the i64-to-i32 cast mangles an absurd value). exa turns that None into a panic while formatting a full-iso timestamp with a timezone. The offset value itself comes from the TimeZone loaded at startup from the $TZ environment variable or /etc/localtime (src/output/table.rs:342-360), so the panic signals that the loaded zone data produced an impossible offset.
Source
Thrown at src/output/time.rs:130
date.year(), date.month() as usize, date.day(),
date.hour(), date.minute())
}
#[allow(trivial_numeric_casts)]
fn full_local(time: SystemTime) -> String {
let date = LocalDateTime::at(systemtime_epoch(time));
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}",
date.year(), date.month() as usize, date.day(),
date.hour(), date.minute(), date.second(), systemtime_nanos(time))
}
#[allow(trivial_numeric_casts)]
fn full_zoned(time: SystemTime, zone: &TimeZone) -> String {
use datetime::Offset;
let local = LocalDateTime::at(systemtime_epoch(time));
let date = zone.to_zoned(local);
let offset = Offset::of_seconds(zone.offset(local) as i32).expect("Offset out of range");
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {:+03}{:02}",
date.year(), date.month() as usize, date.day(),
date.hour(), date.minute(), date.second(), systemtime_nanos(time),
offset.hours(), offset.minutes().abs())
}
#[allow(trivial_numeric_casts)]
fn iso_local(time: SystemTime) -> String {
let date = LocalDateTime::at(systemtime_epoch(time));
if is_recent(&date) {
format!("{:02}-{:02} {:02}:{:02}",
date.month() as usize, date.day(),
date.hour(), date.minute())
}
else {
format!("{:04}-{:02}-{:02}",
date.year(), date.month() as usize, date.day())View on GitHub (pinned to 3d1edbb470)
Solutions
- Test the environment first: run with TZ=UTC (for example TZ=UTC exa --time-style=full-iso). If that works, your previous $TZ or /etc/localtime is the culprit.
- Point $TZ at a known-good zone file or reinstall tzdata (Debian/Ubuntu: apt install --reinstall tzdata; Fedora: dnf reinstall tzdata), and verify with file /etc/localtime and by diffing against /usr/share/zoneinfo/UTC.
- If you embed this code: replace the expect with a safe fallback (offset 0 or fall back to the unzoned full_local format) so one bad zone cannot abort the whole listing.
- If the panic persists on valid tzdata, report it upstream with the exact $TZ value and the zone file, since zone.offset() returning an out-of-range value from a valid TZif would be a datetime-crate bug.
Example fix
// before (src/output/time.rs)
let offset = Offset::of_seconds(zone.offset(local) as i32).expect("Offset out of range");
// after: degrade gracefully instead of panicking
let offset = Offset::of_seconds(zone.offset(local) as i32)
.unwrap_or_else(|| Offset::of_seconds(0).expect("UTC offset is always in range"));
// or: fall back to the unzoned formatter
// if Offset::of_seconds(zone.offset(local) as i32).is_none() {
// return full_local(time);
// } Defensive patterns
Strategy: fallback
Validate before calling
// Before rendering with --time-style=full-iso, sanity-check the active zone:
use datetime::{LocalDateTime, TimeZone, Offset};
use std::time::SystemTime;
fn zone_offset_is_representable(zone: &TimeZone) -> bool {
let now = LocalDateTime::at(
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap()
);
let secs = zone.offset(now);
// Offset::of_seconds rejects anything beyond +/- 24 hours (i32 range)
secs.abs() < 24 * 60 * 60 && Offset::of_seconds(secs as i32).is_some()
}
if !zone_offset_is_representable(&tz) {
// fall back to UTC or to the unzoned formatter before rendering
} Type guard
fn usable_for_full_iso(zone: &TimeZone) -> bool {
// Probe the exact conversion that full_zoned() (src/output/time.rs:130)
// performs with .expect(); if it would be None, exa would panic.
let local = LocalDateTime::at(std::time::Duration::from_secs(0));
Offset::of_seconds(zone.offset(local) as i32).is_some()
} Try / catch
// The failure is a panic, not an Err; guard with catch_unwind and keep listing:
let formatted = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
TimeFormat::FullISO.format_zoned(time, &zone)
})).unwrap_or_else(|_| {
TimeFormat::FullISO.format_local(time) // fallback: drop the zone suffix
}); Prevention
- Pin $TZ to a known-good zone (TZ=UTC) in scripts and containers that parse exa output, instead of relying on inherited /etc/localtime.
- After building minimal container images, verify the tzdata payload (file /etc/localtime; compare checksums with /usr/share/zoneinfo/UTC) before shipping.
- Treat 'Offset out of range' as a data-integrity signal: reinstall tzdata rather than working around individual files.
- If you embed exa's time formatting, prefer format_local() when the environment's zone file is not under your control, or wrap format_zoned() in the catch_unwind fallback shown above.
When it happens
Trigger: Running exa with --time-style=full-iso (full_zoned is the only formatting path that calls Offset::of_seconds) on Unix, in a process where determine_time_zone() succeeded but the zone data is broken: a corrupt or truncated TZif file, a hand-crafted/custom zone file with offsets at or beyond ±86400 seconds, or $TZ pointing at such a file. The panic happens per file, at the moment its mtime is formatted.
Common situations: Systems with a damaged /etc/localtime (interrupted tzdata update, container image with a truncated zoneinfo file), $TZ pointing to a stale custom file, or minimal containers where /usr/share/zoneinfo is partially copied. Rare with stock distro tzdata, because real zone offsets stay within ±14 hours; essentially always an environment/data-integrity problem rather than a code bug in the caller.
Related errors
AI-assisted analysis of ogham/exa@3d1edbb470 (2026-08-16).
Data as JSON: /api/errors/c07ff6c21977a4d9.
Report an issue: GitHub.