Back to Blog

Time zones in JavaScript — Date, Intl, and the upcoming Temporal API

April 11, 2026
3 min read
Written by ConvertTime Team
javascripttime-zonesdevelopersfrontendintl
JavaScript's `Date` object is one of the most-criticized APIs in the language. It's based on the original Java Date class (which Java itself replaced). It's confusing, error-prone, and time-zone-handling is particularly painful. Modern JavaScript has better tools — `Intl.DateTimeFormat` for formatting and the upcoming `Temporal` API for everything else. For converting Unix timestamps to/from JavaScript dates, the [epoch converter](/calculators/epoch) handles them. ## The Date object's problems JavaScript's `new Date()` works by: - Storing time as milliseconds since 1970-01-01 UTC (a 64-bit integer) - Displaying time according to the system's local time zone This means: ```javascript const d = new Date('2026-05-21T14:30:00Z'); console.log(d.toString()); // Output depends on your system zone — could be: // "Thu May 21 2026 10:30:00 GMT-0400 (Eastern Daylight Time)" // or // "Thu May 21 2026 19:30:00 GMT+0500 (India Standard Time)" // or many other things console.log(d.getHours()); // Returns local hour, not UTC console.log(d.getUTCHours()); // Returns UTC hour ``` The local-vs-UTC inconsistency causes countless bugs. ## What works in modern JavaScript For formatting dates to specific time zones, `Intl.DateTimeFormat` is reliable: ```javascript const date = new Date('2026-05-21T14:30:00Z'); const tokyoFormatter = new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Tokyo', dateStyle: 'medium', timeStyle: 'short' }); console.log(tokyoFormatter.format(date)); // "May 21, 2026, 11:30 PM" ``` This works regardless of the system's local zone. For parsing time-zone-aware strings: ```javascript // ISO 8601 with offset const d = new Date('2026-05-21T14:30:00+05:30'); // d represents the absolute UTC moment 2026-05-21T09:00:00Z // ISO 8601 without offset (interpreted as local time) const d2 = new Date('2026-05-21T14:30:00'); // d2 is 2:30 PM in your system's local zone — could mean different UTC moments ``` For arithmetic, `Date` is dangerous: ```javascript const d = new Date('2026-03-08T01:30:00'); // Spring DST shift day d.setHours(d.getHours() + 1); // Result: depends on time zone. In US Eastern, this might be 02:30 (skipped) or 03:30 (next valid hour) ``` Use a library or wait for `Temporal`. ## Intl.DateTimeFormat in detail The most reliable formatting API: ```javascript // Specific time zone new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Tokyo', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: false }).format(date); // "May 21, 2026, 23:30" // Locale-aware formatting new Intl.DateTimeFormat('ja-JP', { timeZone: 'Asia/Tokyo', dateStyle: 'medium', timeStyle: 'short' }).format(date); // "2026/05/21 23:30" // Get parts for custom formatting const parts = new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }).formatToParts(date); // Array of {type: 'year', value: '2026'}, {type: 'month', value: '05'}, etc. ``` `Intl.DateTimeFormat` handles: - Time zones (including DST) - Locale-specific formatting - Multiple precision levels - Custom formats ## The upcoming Temporal API Temporal is a proposal that's nearly complete (Stage 3 → Stage 4). It addresses Date's problems with multiple types: ```javascript // PlainDate — no time, no zone const date = Temporal.PlainDate.from('2026-05-21'); // PlainTime — time only const time = Temporal.PlainTime.from('14:30:00'); // PlainDateTime — date + time, no zone const datetime = Temporal.PlainDateTime.from('2026-05-21T14:30:00'); // ZonedDateTime — date + time + zone (the most common) const zoned = Temporal.ZonedDateTime.from('2026-05-21T14:30:00[Asia/Tokyo]'); // Instant — UTC moment const instant = Temporal.Instant.from('2026-05-21T14:30:00Z'); ``` Temporal handles arithmetic correctly: ```javascript const next = zoned.add({ days: 7 }); // Adds 7 days, handling DST correctly const diff = zoned.until(otherZoned); // Returns precise difference, handling DST ``` When Temporal launches in browsers (expected 2025-2026), most JavaScript date code should migrate. ## Library alternatives Until Temporal lands, several libraries fill the gap: **date-fns**: functional, no time zone support by default. With `date-fns-tz` extension for zones. ```javascript import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz'; const tokyoTime = utcToZonedTime(new Date(), 'Asia/Tokyo'); ``` **Luxon**: time-zone-aware out of the box. Successor to Moment.js. ```javascript import { DateTime } from 'luxon'; const dt = DateTime.fromISO('2026-05-21T14:30:00Z') .setZone('Asia/Tokyo'); console.log(dt.toString()); // "2026-05-21T23:30:00+09:00" ``` **Day.js**: lightweight, with `dayjs/plugin/timezone` for zone support. ```javascript import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; dayjs.extend(utc); dayjs.extend(timezone); const d = dayjs.tz('2026-05-21T14:30:00Z', 'Asia/Tokyo'); ``` **Moment.js**: the original time-zone library. Still maintained but in maintenance mode (recommends migration to alternatives). With `moment-timezone` for zones. For new projects, prefer Luxon or wait for Temporal. ## Common JavaScript time-zone patterns ### Get the user's time zone ```javascript const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; // "America/New_York" or whatever ``` ### Convert UTC to user's local ```javascript const utcDate = new Date('2026-05-21T14:30:00Z'); const localFormatted = utcDate.toLocaleString(); // Output in user's local format ``` ### Convert across zones ```javascript const date = new Date('2026-05-21T14:30:00Z'); const tokyoString = new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Tokyo' }).format(date); // "5/21/2026, 11:30:00 PM" ``` ### Compare across zones ```javascript const a = new Date('2026-05-21T14:30:00Z'); const b = new Date('2026-05-21T19:30:00Z'); console.log(a < b); // true — comparing UTC moments, time zones don't matter ``` ### Get a specific zone's "today" ```javascript function todayInZone(timeZone) { return new Intl.DateTimeFormat('en-US', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date()); // Returns "2026/05/21" in target zone } ``` ## Common mistakes **Using `getHours()` etc. when you mean UTC**: use `getUTCHours()` or `Intl.DateTimeFormat`. **Concatenating Date strings with assumptions about format**: `Date.toString()` output depends on system locale and zone. Use `toISOString()` for stable output. **Date arithmetic with `setDate()`**: dangerous around DST. Use library or Temporal. **Trusting `new Date('2026-05-21')` to be midnight UTC**: it's actually midnight LOCAL time. **Storing dates as strings in random formats**: always store as ISO 8601 (or RFC 3339). ## Specific gotchas ### `new Date('2026-05-21')` parses as UTC ```javascript new Date('2026-05-21').toISOString(); // "2026-05-21T00:00:00.000Z" — midnight UTC ``` ### `new Date('2026-05-21T14:30:00')` parses as LOCAL ```javascript // In Eastern time: new Date('2026-05-21T14:30:00').toISOString(); // "2026-05-21T18:30:00.000Z" — interpreted as 14:30 EDT ``` The difference is the time component. Date-only strings are UTC. With time, ISO 8601 strings without offset are local. ### Cross-browser inconsistency Older browsers parse some date strings differently from newer ones. Always use ISO 8601 strict format for input. ### Performance `Intl.DateTimeFormat` creation is relatively slow. Cache formatters: ```javascript const formatter = new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Tokyo' }); // Reuse for many calls ``` ## FAQ ### What's the right way to handle time zones in JavaScript? For now: use `Intl.DateTimeFormat` for formatting and a library (Luxon, day.js) for arithmetic. After Temporal lands, use Temporal directly. ### Can I trust `Date` for arithmetic? Generally no. `Date.setDate()` and similar can give wrong results across DST boundaries. Use a library. ### When will Temporal be available? It's at Stage 3 of the TC39 process. Polyfills are available now. Browser support is rolling out in 2025-2026. Production-ready in late 2025 or early 2026 for most use cases. ### Should I use Moment.js? Not for new projects. Moment recommends migrating to alternatives (Luxon, day.js, date-fns). It's still functional but not actively developed. ### How do I handle time zones on the server vs client? Server: Node.js has the same `Date` and `Intl` as browsers. Use UTC internally and convert at the response boundary. Client: receive UTC timestamps, convert to user's local zone for display using `Intl`. ### What about Date.now()? `Date.now()` returns milliseconds since epoch (Unix timestamp × 1000). It's reliable for timestamps. The output is always UTC-anchored. ## Bottom line JavaScript's Date is problematic for time zones. Use `Intl.DateTimeFormat` for formatting and a modern library (Luxon, day.js) for arithmetic. The upcoming Temporal API will fix most of these issues; expect adoption in 2025-2026. For converting Unix timestamps to/from JavaScript dates, the [epoch converter](/calculators/epoch) handles them.

Share this article

Related Articles