Working with Timezones
Timezone Bugs Are Expensive
A timezone bug does not crash your app — it silently corrupts data. Scheduled emails arrive at the wrong hour. Subscription renewals fire a day early. Audit logs show timestamps that do not match reality. By the time you notice, bad data has spread across systems.
The fix starts with principles, not libraries.
Always Store in UTC
UTC never observes Daylight Saving Time. It never shifts. A UTC timestamp means the same thing on every server, in every database, in every country.
# BAD — storing local time
event_time = "2026-05-28 14:00:00" # What zone?
# GOOD — storing UTC
event_time = "2026-05-28T18:00:00Z" # 2 PM EDT = 6 PM UTC
Rule: Store every timestamp in UTC. No exceptions. Convert to local time only at the point of display.
Display in Local Time
Users do not think in UTC. When you show 2026-05-28T18:00:00Z to someone in Tokyo, they have to do mental math. Display times in the user's local timezone instead.
// Convert UTC to the user's local timezone
const date = new Date("2026-05-28T18:00:00Z")
console.log(date.toLocaleString())
// In Tokyo: "2026/5/29 03:00:00" (next day!)
// In New York: "5/28/2026, 2:00:00 PM"
JavaScript's Date object converts to the browser's local timezone automatically when you use toLocaleString() or toLocaleDateString().
ISO 8601 Format
ISO 8601 is the international standard for date and time representation. It eliminates ambiguity.
2026-05-28T18:00:00Z (UTC, denoted by Z)
2026-05-28T14:00:00-04:00 (with offset)
2026-05-28 (date only — no ambiguity)
Why ISO 8601 wins:
- Alphabetical sort equals chronological sort
- Every component has a fixed position
- Timezone offset is explicit
- Supported by every major language and database
Always include the offset (Z or +/-HH:MM). A bare string like 2026-05-28T14:00:00 is ambiguous — different systems interpret it as local or UTC inconsistently.
JavaScript Intl API
Modern browsers provide the Intl API for timezone-aware formatting — no libraries required.
Format in a Specific Timezone
const date = new Date("2026-05-28T18:00:00Z")
const londonTime = new Intl.DateTimeFormat("en-GB", {
timeZone: "Europe/London",
dateStyle: "full",
timeStyle: "short",
}).format(date)
// "Thursday, 28 May 2026 at 19:00" (BST, UTC+1)
List Available Timezones
const zones = Intl.supportedValuesOf("timeZone")
// ["Africa/Abidjan", "Africa/Accra", ..., "America/New_York", ...]
Get the Offset for a Specific Date
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
timeZoneName: "shortOffset",
})
formatter.format(new Date("2026-01-15T12:00:00Z"))
// "1/15/2026, GMT-5" (EST)
formatter.format(new Date("2026-07-15T12:00:00Z"))
// "7/15/2026, GMT-4" (EDT)
The Intl API handles DST automatically. Use it for display wherever possible.
Timezone Libraries
The Intl API covers most display needs, but complex operations — arithmetic, parsing, range queries — benefit from dedicated libraries.
| Library | Size | Strengths | Weaknesses |
|---|---|---|---|
date-fns-tz | ~3 KB | Modular, tree-shakable, immutable | Requires date-fns base |
luxon | ~18 KB | Rich API, built on Intl, timezone-first | Larger bundle than alternatives |
dayjs + plugin | ~7 KB | Moment-compatible API, lightweight | Timezone support via optional plugin |
Avoid moment.js. It is deprecated, mutable, and 67 KB uncompressed. Modern alternatives are smaller, immutable, and actively maintained.
date-fns-tz Example
import { formatInTimeZone } from "date-fns-tz"
const date = new Date("2026-05-28T18:00:00Z")
formatInTimeZone(date, "Asia/Tokyo", "yyyy-MM-dd HH:mm zzz")
// "2026-05-29 03:00 JST"
Luxon Example
import { DateTime } from "luxon"
DateTime.fromISO("2026-05-28T18:00:00Z")
.setZone("America/Los_Angeles")
.toFormat("yyyy-MM-dd hh:mm a ZZZZ")
// "2026-05-28 11:00 AM PDT"
Common Timezone Bugs and Fixes
Bug: Adding Hours Across DST
// BAD — adds 24 hours in local time, which may be 23 or 25 real hours
const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
Fix: Work in UTC for arithmetic, then convert to local for display.
Bug: Parsing Without an Offset
// BAD — JavaScript interprets this as local time
new Date("2026-05-28T14:00:00")
// Result varies by machine timezone
// GOOD — explicit UTC
new Date("2026-05-28T14:00:00Z")
// Same result everywhere
Bug: Storing Timezone Names Separately
// BAD
{ "time": "2026-05-28T14:00:00", "tz": "America/New_York" }
// GOOD — the offset is part of the timestamp
{ "time": "2026-05-28T14:00:00-04:00" }
Storing the timezone separately forces every consumer to recompute the offset. Embedding the offset in the timestamp makes the value self-describing.
Quick Checklist
- All timestamps stored in UTC
- ISO 8601 format with offset (
Zor+/-HH:MM) used everywhere - Local display uses
IntlAPI or a timezone-aware library - No hardcoded UTC offsets in configuration
- No
moment.jsin dependencies - Date arithmetic performed in UTC, not local time
- DST transition dates tested explicitly
Related Guides
Try It Yourself
Use our free Timezone Converter to verify timezone conversions — and the Timestamp Converter to switch between Unix timestamps and readable dates.