← All posts

JavaScript Timezone Guide: UTC, IANA Zones, and Intl.DateTimeFormat

A JavaScript Date stores one timezone-neutral instant as epoch milliseconds. Use toISOString() for UTC, Intl.DateTimeFormat with an IANA timeZone for display, resolvedOptions().timeZone for the user's zone, and Temporal or a timezone library when converting wall-clock time into UTC.

Quick answer

A JavaScript Date does not store a timezone. It stores one instant as epoch milliseconds.

const date = new Date(1700000000000);

date.toISOString();
// "2023-11-14T22:13:20.000Z"

new Intl.DateTimeFormat("en-US", {
  timeZone: "America/New_York",
  dateStyle: "medium",
  timeStyle: "short",
}).format(date);
// "Nov 14, 2023, 5:13 PM"

Use this decision table:

Task Use
Store an exact event time Unix milliseconds, Unix seconds, or ISO 8601 UTC
Display UTC date.toISOString() or timeZone: "UTC"
Display a user's local time Intl.DateTimeFormat(..., { timeZone })
Detect the user's browser timezone Intl.DateTimeFormat().resolvedOptions().timeZone
Convert a local appointment time to UTC Temporal, Temporal polyfill, Luxon, or date-fns-tz
Handle recurring meetings Store date, time, and IANA timezone name

The mental model: instant vs display

Most JavaScript timezone bugs come from mixing up two different things:

  • An instant: one exact moment, such as 2023-11-14T22:13:20.000Z.
  • A wall-clock display: what a person sees in a place, such as 5:13 PM in New York.

Date represents the instant. It does not remember whether the user meant New York, Tokyo, UTC, or the server's local timezone.

const date = new Date("2023-11-14T22:13:20Z");

date.getTime();
// 1700000000000

That number is the same everywhere. The displayed string changes only when you choose a timezone.

MDN describes Date as milliseconds since the Unix epoch, and the timestamp is timezone-agnostic. The local methods read the host system timezone when they need calendar fields or a string.

Format UTC in JavaScript

For logs, API debugging, tests, database snapshots, and docs, UTC is usually the safest display.

const date = new Date(1700000000000);

date.toISOString();
// "2023-11-14T22:13:20.000Z"

If your input is Unix seconds, multiply by 1000 first:

const seconds = 1700000000;
new Date(seconds * 1000).toISOString();
// "2023-11-14T22:13:20.000Z"

If you want a localized UTC string instead of ISO format, use Intl.DateTimeFormat:

const utcFormatter = new Intl.DateTimeFormat("en-US", {
  timeZone: "UTC",
  dateStyle: "medium",
  timeStyle: "long",
});

utcFormatter.format(new Date(1700000000000));
// "Nov 14, 2023, 10:13:20 PM UTC"

Format a Date in an IANA timezone

For user-facing output, pass an IANA timezone name to Intl.DateTimeFormat.

const date = new Date("2023-11-14T22:13:20Z");

const fmt = new Intl.DateTimeFormat("en-US", {
  timeZone: "America/New_York",
  dateStyle: "full",
  timeStyle: "long",
});

fmt.format(date);
// "Tuesday, November 14, 2023 at 5:13:20 PM EST"

Good timezone values:

  • UTC
  • America/New_York
  • America/Los_Angeles
  • Europe/London
  • Europe/Berlin
  • Asia/Tokyo
  • Asia/Shanghai

Avoid storing EST, PST, GMT-5, or -05:00 as the user's timezone. Those are offsets or abbreviations, not a full set of timezone rules. America/New_York knows that January is usually UTC-5 and July is usually UTC-4.

Construct formatter instances once when formatting many rows:

const fmt = new Intl.DateTimeFormat("en-US", {
  timeZone: "Asia/Tokyo",
  dateStyle: "medium",
  timeStyle: "short",
});

rows.map((row) => fmt.format(new Date(row.createdAtMs)));

Get the user's timezone

Use resolvedOptions() in browser code:

const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// "America/Los_Angeles"

Important details:

  • This is the browser or runtime's current default timezone.
  • It can change if the user changes their operating-system or browser setting.
  • It may be UTC in containers, CI, or locked-down environments.
  • Server-side code sees the server timezone, not the user's timezone.

For a signup form, scheduling app, or report preference, send the browser-detected IANA name to your API and let the user override it.

await fetch("/api/profile/timezone", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ timeZone: userTimeZone }),
});

To build a timezone picker, modern runtimes support:

const zones = Intl.supportedValuesOf("timeZone");

Still validate stored values, because old browsers and unusual runtimes may have partial timezone data.

Use formatToParts for custom output

Do not split localized strings with /, -, or commas. Locale formatting changes order, punctuation, script, and spacing.

Use formatToParts() when you need your own layout:

const parts = new Intl.DateTimeFormat("en-US", {
  timeZone: "America/Chicago",
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit",
  hour12: false,
}).formatToParts(new Date("2026-06-20T09:25:00Z"));

const byType = Object.fromEntries(parts.map((part) => [part.type, part.value]));

`${byType.year}-${byType.month}-${byType.day} ${byType.hour}:${byType.minute}`;
// "2026-06-20 04:25"

This is useful for CSV exports, admin tables, and date labels where product design requires a fixed shape but the timezone still needs to be correct.

Do not use getTimezoneOffset for other zones

getTimezoneOffset() is often misunderstood. It returns the host runtime's local offset for that instant.

const date = new Date("2026-01-15T12:00:00Z");
date.getTimezoneOffset();
// depends on the machine running this code

It does not answer "what is the offset in New York?" unless the machine itself is configured to New York.

The sign is also easy to read backward:

  • UTC-8 returns 480
  • UTC returns 0
  • UTC+3 returns -180

The value can vary by date in daylight-saving regions. If the host is in New York, January and July usually have different offsets.

Use getTimezoneOffset() only when you intentionally need the runtime's local offset. Use Intl.DateTimeFormat or Temporal for named timezones.

Parse strings safely

Parsing is where timezone bugs sneak in quietly.

Use strings with Z or an explicit offset:

new Date("2026-06-20T09:25:00Z");      // UTC
new Date("2026-06-20T09:25:00-04:00"); // explicit offset

Be careful with date-time strings that omit a timezone:

new Date("2026-06-20T09:25:00");
// interpreted in the runtime's local timezone

One subtle rule from the JavaScript date-time string format: date-only strings such as 2019-01-01 are interpreted as UTC, while date-time strings without an offset such as 2019-01-01T00:00:00 are interpreted as local time.

Avoid these for system boundaries:

new Date("06/20/2026");
new Date("Jun 20, 2026");
new Date("2026/06/20 09:25");

Those formats are implementation-defined. They may work in one engine and behave differently in another.

Convert wall-clock time to UTC

Formatting an existing instant is easy:

fmt.format(new Date("2026-06-20T09:25:00Z"));

Creating an instant from a local wall-clock time is harder:

2026-03-08 02:30 in America/New_York

That local time falls in the US spring-forward gap. It does not exist. In the fall-back overlap, a time like 01:30 can happen twice.

The old Date API has no clean built-in way to construct an instant from a wall-clock time plus an IANA timezone name. Practical options:

Need Use
Display an existing instant in a timezone Intl.DateTimeFormat
Convert local wall-clock time plus IANA zone to an instant Temporal, Temporal polyfill, Luxon, or date-fns-tz
Reject nonexistent or ambiguous local times Temporal with disambiguation: "reject"
Keep old browser support without a polyfill a timezone-aware library

Temporal's ZonedDateTime is designed for this job. MDN still marks Temporal as limited availability, but Node.js 26 enables it by default. For public web apps, feature-detect or ship a polyfill before relying on it everywhere.

// Requires native Temporal or the Temporal polyfill.
const zdt = Temporal.ZonedDateTime.from(
  {
    timeZone: "America/New_York",
    year: 2026,
    month: 11,
    day: 1,
    hour: 1,
    minute: 30,
  },
  { disambiguation: "later" },
);

zdt.toInstant().epochMilliseconds;

Temporal supports earlier, later, compatible, and reject for DST gaps and overlaps. That explicit choice is the real win.

What to store in your database

Store different data depending on the product question.

Product meaning Store
"This payment happened at one exact instant" UTC timestamp or ISO string with Z
"Show this event in the user's local timezone" UTC instant plus user's IANA timezone
"Meeting every Monday at 9 AM in New York" local date/time rule plus America/New_York
"A date with no time, like birthday" date-only value, not a JavaScript Date at midnight
"A log line from a server" UTC timestamp and server metadata

Examples:

{
  "createdAt": "2026-06-20T09:25:00Z",
  "createdAtMs": 1781947500000
}

For scheduled local events:

{
  "localDate": "2026-11-01",
  "localTime": "01:30",
  "timeZone": "America/New_York",
  "disambiguation": "later"
}

Do not store only -04:00 or -05:00 for future local events. Timezone laws can change, and offsets do not contain DST rules.

DST gap and overlap in real terms

Daylight saving transitions break the idea that every local clock time maps to exactly one instant.

For America/New_York:

  • Spring-forward gap: 2026-03-08 02:00 through 02:59 local time does not exist.
  • Fall-back overlap: 2026-11-01 01:00 through 01:59 local time happens twice.

Unix timestamps do not skip or repeat. UTC keeps moving. The weirdness appears only when you interpret that instant as local wall-clock time.

Test timezone code with the transition dates for the target zone. A scheduler that works on an ordinary Wednesday can still fail on the two DST Sundays.

Common JavaScript timezone mistakes

Mistake Why it breaks Fix
new Date("2026-06-20T09:25:00") in shared code no timezone means local runtime timezone include Z or an explicit offset
date.toLocaleString() on the server uses server/container timezone pass timeZone explicitly
storing -05:00 as the user's timezone offset loses DST rules store America/New_York
using getTimezoneOffset() for another city it only knows the host timezone use IANA timeZone formatting
subtracting 5 * 60 * 60 * 1000 for New York New York is not always UTC-5 use IANA timezone rules
storing a local display string as truth cannot reliably convert back store UTC instant plus timezone context
assuming Temporal exists in every browser still limited availability on MDN feature-detect or polyfill
using Moment for new code project is in maintenance mode use Intl, Temporal, Luxon, or date-fns-tz

Moment's own docs describe it as a legacy project in maintenance mode. Existing apps can keep it while migrating carefully, but new timezone code should start elsewhere.

Official references

Related timestamp guides

FAQ

Does a JavaScript Date store a timezone?
No. A Date stores one timestamp value: milliseconds since 1970-01-01 00:00:00 UTC. Local time, UTC, and named timezones are display choices applied later.
Is a JavaScript timestamp always UTC?
The timestamp is counted from the UTC Unix epoch and is timezone-neutral. It is not stored as New York time, Tokyo time, or server local time. Timezone only appears when the instant is formatted.
How do I format a Date in UTC?
Use date.toISOString() for a UTC ISO 8601 string ending in Z. For localized UTC display, use Intl.DateTimeFormat with timeZone: 'UTC'.
How do I format a Date in another timezone?
Use Intl.DateTimeFormat with an IANA timezone such as America/New_York, Europe/London, or Asia/Tokyo. The runtime applies the correct offset for that date, including daylight saving rules.
How do I get the user's timezone in JavaScript?
Use Intl.DateTimeFormat().resolvedOptions().timeZone. It returns the runtime's default IANA timezone, such as America/Los_Angeles, when the environment exposes one.
Should I store a timezone offset or an IANA timezone name?
Store UTC for exact instants. If you also need the user's local context, store an IANA timezone name, not just an offset like -05:00. Offsets do not carry daylight-saving or political rule changes.
Why does the same Date show different output on my laptop and server?
Methods like toString() and toLocaleString() use the runtime's default timezone unless you pass an explicit timeZone. Your laptop, container, CI runner, and production server may all have different defaults.
Is getTimezoneOffset() the offset for a Date's timezone?
No. getTimezoneOffset() returns the host runtime's local offset for that instant. It does not tell you the offset for America/New_York unless the host itself is using America/New_York.
Can JavaScript convert a wall-clock time in an IANA timezone to UTC without a library?
Not cleanly with Date alone. Intl can format an existing instant in a timezone, but Date cannot directly construct 2026-03-08 02:30 in America/New_York and resolve DST ambiguity. Use Temporal where available, the Temporal polyfill, or a timezone-aware library.
Should I use Temporal for JavaScript timezone code?
Use Temporal for timezone arithmetic and wall-clock-to-instant conversion where it is available or where you can ship a polyfill. As of July 15, 2026, MDN still marks Temporal as limited availability, while Node.js 26 enables it by default.