datestimezoneutcjavascript

Time zones in software: why your app stores dates wrong

UTC, offsets, DST and the golden rule of dates: how to store, calculate and display times correctly plus the classic time zone bugs.

August 24, 2026·8 min read

"Why does my 9:00 meeting show as 8:00?" "Yesterday's report includes today's data." Date bugs are universal because they look trivial and aren't: behind a date there's physics (Earth rotates imperfectly), politics (governments change DST rules) and history (offsets with seconds and minutes). This article gives the right mental model and the practical rules eliminating 95% of these bugs.

The mental model: instant ≠ representation

Everything starts by separating two concepts everyday language blends:

  • Instant: an absolute point on the timeline. "The moment it happened". It has no time zone.
  • Civil representation: how a human in a specific place labels that instant. "March 9th at 9:00 in Madrid".

The single golden rule of this article: store and transmit instants; convert to representation only when displaying.

An instant is canonically represented as UTC or a Unix timestamp:

2026-08-24T07:00:00Z        ← ISO 8601, the Z means UTC
1756018800                  ← seconds since 1970-01-01T00:00:00Z

Both identify the exact same second with no possible ambiguity.

The foundational error: storing local times

An app saving "2026-08-24 09:00" without a zone has lost information forever. When another user —or the same one after flying— queries that data, 09:00 where? Typical consequences:

  • Servers on UTC and users in Spain: everything shifted 1-2 hours depending on season.
  • Duplicated or missing daily-report data when DST moves the clock.
  • Cross-origin record comparisons that never add up.

And the trap case: it works perfectly for months... until the March or October time change, when the offset jumps. It's the bug appearing twice a year, always in production.

Offset ≠ time zone

A distinction solving half the confusion: +02:00 is an offset, a number. Europe/Madrid is a time zone: a historical and future rule stating which offset applies at each date.

Why it matters: Spain is +02:00 in summer and +01:00 in winter. If you store the fixed offset instead of the zone, your data freezes in eternal summer. Zones also embed political decisions: Spain lived on solar time until 1940 and skipped an hour overnight when aligning with CET. The IANA database (tzdata) maintains that whole history per region — updated several times a year because governments keep changing rules.

Derived rules:

  1. Never compute offsets by hand or hardcode them.
  2. Store IANA identifiers (Europe/Madrid, America/New_York), not ambiguous abbreviations (CST means three different things by country).
  3. Keep tzdata updated on servers and OS.

DST: the hour that happens twice (and the one that never happens)

Clock changes create two mathematical anomalies breaking naive arithmetic:

In autumn (clocks back): 02:30 occurs twice — first at +02:00, then at +01:00. Adding "one hour" to 02:15 can return the same 02:15.

In spring (clocks forward): 02:30 doesn't exist. A user entering it produces an undefined instant each library resolves differently.

Practical consequence: never do day arithmetic by adding 86,400 seconds. "Tomorrow" is calendar math, not duration:

// WRONG: fails on DST changes
const tomorrow = new Date(date.getTime() + 86400_000);

// RIGHT: calendar arithmetic
const tomorrow = new Date(date);
tomorrow.setDate(tomorrow.getDate() + 1);

JavaScript: the essential minimum

Native Date internally stores a UTC timestamp (milliseconds) — the correct model — but its local methods (getHours(), toLocaleString()) depend on the device's zone, causing surprises on servers (usually running UTC) versus users.

For formatting in a specific zone, modern Intl API:

new Date("2026-08-24T07:00:00Z").toLocaleString("en-US", {
  timeZone: "Europe/Madrid",
  dateStyle: "long",
  timeStyle: "short"
});
// "August 24, 2026 at 9:00 AM"

Since 2025 browsers support Temporal, the API fixing decades of defects: separated types for instants (Temporal.Instant), date without time (PlainDate) and datetime with zone (ZonedDateTime). In new projects where support reaches, it's the serious choice.

To convert and verify concrete timestamps, our Unix timestamp converter shows the same instant in your local zone, in UTC, and its numeric value bidirectionally.

Anti-bug checklist

  • Database: timestamptz columns (Postgres) or pure UTC timestamps; never local datetime without zone.
  • APIs: exchange exclusively in ISO 8601 with Z.
  • Frontend: convert to user's zone only at render; recompute if their zone changes.
  • Reminders/calendars: store civil time + IANA zone ("09:00 Europe/Madrid"), because "9 in the morning" must stay 9 even if the offset changes.
  • Tests: always cover the clock-change day (March/October) and leap years.
  • Logs: always UTC; debugging will thank you.

FAQ

Should I ask users for their time zone? For display, use the auto-detected one (Intl.DateTimeFormat().resolvedOptions().timeZone) changeable in settings. For scheduled events, ask explicitly: the laptop's zone may not be the relevant one.

Does Unix timestamp have a Y2038 problem? The classic signed 32-bit variant runs out in January 2038. Any modern system with 64-bit integers (all current web runtimes) is safe.

What if Spain abolishes clock changes? Exactly what correct design predicts: you update tzdata, and apps that stored IANA zones keep working; those that hardcoded offsets don't.


Convert and verify timestamps with the Unix Timestamp Converter, free and right in your browser.

Try it without code

Timestamp Converter

Unix timestamps to readable dates.

Open Timestamp Converter

Built by

Miguel Ángel Colorado Marin (MACM)

Full-Stack Developer · Guadalajara, España

I develop web apps, digital tools and full projects — from design to deployment.

Contact me