How to Handle Timezones and UTC in JavaScript

Timezones trip up almost every developer at some point, mostly because Date objects don't actually "have" a timezone — internally, every Date is just a millisecond count since the Unix epoch (UTC). Timezones only enter the picture when you format or parse a date. Once you internalize that, most of the confusion goes away.

1. Detect the visitor's timezone

No library needed — the browser already knows:

const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; // e.g. "America/Argentina/Buenos_Aires"

2. Format a date in any timezone

Pass a timeZone to Intl.DateTimeFormat and it handles the conversion, including daylight saving, for you:

new Intl.DateTimeFormat("en-US", { timeZone: "America/New_York", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true, }).format(new Date()); // "3:45:12 PM"

3. Get a timezone's current UTC offset

Useful for labels like "UTC-5" or converting between two zones:

function getOffsetLabel(timeZone, date) { const parts = new Intl.DateTimeFormat("en-US", { timeZone, timeZoneName: "shortOffset", }).formatToParts(date); return parts.find(p => p.type === "timeZoneName").value; // "GMT-5" }

4. Convert a wall-clock time from one zone to another

This is the part people usually reach for a library for, but it's doable with just the offset above: build a UTC timestamp from the target date/time, then subtract the source zone's offset.

function convert(zoneA, hh, mm, refDate) { // Y/M/D "today" as seen in zoneA const parts = new Intl.DateTimeFormat("en-CA", { timeZone: zoneA, year: "numeric", month: "2-digit", day: "2-digit", }).formatToParts(refDate); const get = t => +parts.find(p => p.type === t).value; const offsetMin = /* parse "-300" from getOffsetLabel(zoneA, refDate) */ -300; const utcMs = Date.UTC(get("year"), get("month") - 1, get("day"), hh, mm) - offsetMin * 60000; return new Date(utcMs); // format this in zoneB to get the converted time }

This is exactly the technique behind our own Meeting Planner tool — worth a look if you'd rather not write the conversion logic yourself.

5. The one rule that prevents most bugs

Store and transmit dates in UTC (ISO 8601, e.g. 2026-07-22T14:30:00Z), and only convert to a local timezone at the last moment — when you render it for a human. Databases, APIs, and logs should never store "local time" without an offset attached; that ambiguity is where most timezone bugs come from.

6. Detect whether a timezone observes daylight saving at all

You don't need a lookup table for this either — compare a zone's UTC offset in mid-January to its offset in mid-July. If they match, that zone doesn't observe daylight saving; if they differ, you also know which offset is currently active. This is the exact technique behind the live status on our own DST Tracker:

function offsetMinutes(timeZone, date) { const label = getOffsetLabel(timeZone, date); // from step 3, e.g. "GMT-5" const m = /GMT([+-])(\d+)(?::(\d+))?/.exec(label); const sign = m[1] === "-" ? -1 : 1; return sign * (parseInt(m[2]) * 60 + (m[3] ? parseInt(m[3]) : 0)); } const year = new Date().getFullYear(); const jan = offsetMinutes("Europe/London", new Date(year, 0, 15)); const jul = offsetMinutes("Europe/London", new Date(year, 6, 15)); const observesDst = jan !== jul; // true for London, false for Tokyo

Common Pitfalls

A handful of mistakes account for most timezone bugs in JavaScript, and none of them need a library to avoid:

  • Date strings without a "Z" or offset are parsed as local time, not UTC. new Date("2026-07-22T14:30:00") is interpreted in the visitor's own timezone, while new Date("2026-07-22T14:30:00Z") is UTC — an easy one-line bug that silently shifts every timestamp by the visitor's offset.
  • getMonth() is zero-indexed. January is 0, December is 11. This is a decades-old JavaScript design choice, not a bug, but it still catches people off guard when building dates by hand.
  • An hour can be skipped or repeated during a DST transition. On the "spring forward" date, the local time 2:30 AM might not exist at all; on "fall back," it can happen twice. Code that builds a Date from raw local hour/minute values without accounting for this can silently land on the wrong side of the transition.
  • toLocaleString() without a timeZone option uses the server's or browser's local zone, not any zone you intended. Always pass an explicit timeZone when formatting for a specific place, especially in server-side Node.js code where "local" means the server's zone, not the user's.

Do you need a library at all?

For most apps, no — Intl and Date cover formatting, conversion, and offset lookups natively in every modern browser and in Node.js. Reach for a library like date-fns-tz or Luxon only if you need heavier date-math (adding business days, recurring events, etc.) — not just timezone display.

Tools

Articles & Guides