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.