Working with Dates Across Time Zones in JavaScript
A practical guide to JavaScript dates — the Date object, UTC vs local time, formatting, parsing, and how to avoid the most common time zone bugs.
How the Date object works internally
A JavaScript Date is always stored internally as a UTC millisecond timestamp (the number of milliseconds since 1970-01-01T00:00:00Z). There is no stored time zone — only the display methods use the local time zone. This is the root of most date bugs: developers confuse the stored UTC value with the local representation.
const d = new Date('2024-01-15T12:00:00Z'); // noon UTC
console.log(d.getTime()); // 1705320000000 — always UTC ms
console.log(d.toISOString()); // '2024-01-15T12:00:00.000Z' — always UTC
console.log(d.toString()); // local time (machine-dependent)
console.log(d.toUTCString()); // UTC stringParsing dates safely
Date string parsing is notoriously inconsistent across engines. The safest rule: only parse ISO 8601 strings, and always include a time zone offset. A date-only string like '2024-01-15' is parsed as UTC midnight in modern browsers but as local midnight in some environments — a common off-by-one-day bug.
// Safe — explicit UTC offset
new Date('2024-01-15T00:00:00Z'); // midnight UTC
new Date('2024-01-15T00:00:00+05:30'); // midnight IST
// Risky — no time component
new Date('2024-01-15'); // UTC in browsers, local in Node
// Avoid — non-ISO formats are implementation-defined
new Date('January 15, 2024'); // works in Chrome, may fail elsewhereFormatting dates for display
Use Intl.DateTimeFormat for locale-aware formatting — it handles time zones, 12/24-hour clocks, and language-specific formats. Avoid building date strings manually with getMonth() (0-indexed!) or getDate().
const d = new Date('2024-01-15T12:00:00Z');
// Format in a specific time zone and locale
const fmt = new Intl.DateTimeFormat('en-GB', {
timeZone: 'Asia/Kolkata',
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit', timeZoneName: 'short',
});
console.log(fmt.format(d));
// '15 Jan 2024, 17:30 IST'
// Quick ISO string
d.toISOString(); // '2024-01-15T12:00:00.000Z'
// Locale default
d.toLocaleDateString('en-US', { timeZone: 'America/New_York' });
// '1/15/2024'Date arithmetic
Do arithmetic on timestamps (milliseconds), not on date components. Adding days by incrementing getDate() fails at month boundaries — use a library or operate on the underlying number.
const MS_PER_DAY = 86_400_000;
// Add 7 days
const d = new Date('2024-01-15T00:00:00Z');
const plus7 = new Date(d.getTime() + 7 * MS_PER_DAY);
console.log(plus7.toISOString()); // 2024-01-22T00:00:00.000Z
// Difference in days between two dates
const a = new Date('2024-01-01T00:00:00Z');
const b = new Date('2024-03-15T00:00:00Z');
const diffDays = (b - a) / MS_PER_DAY;
console.log(diffDays); // 74The Temporal API (modern replacement)
The TC39 Temporal API is available in modern browsers and Node.js 22+ (behind a flag in earlier versions). It was designed to fix all of Date's problems: explicit time zones, unambiguous parsing, immutable values, and proper calendar support. Use it for new code where available.
// Temporal — explicit, unambiguous
const instant = Temporal.Instant.from('2024-01-15T12:00:00Z');
const zoned = instant.toZonedDateTimeISO('Asia/Kolkata');
console.log(zoned.toString());
// '2024-01-15T17:30:00+05:30[Asia/Kolkata]'
// Add duration
const later = zoned.add({ days: 7, hours: 2 });
// Plain date (no time zone)
const date = Temporal.PlainDate.from('2024-01-15');
console.log(date.add({ months: 1 }).toString()); // '2024-02-15'Why is getMonth() 0-indexed?
A historical quirk inherited from Java's java.util.Date. Months are 0–11 (January = 0, December = 11) but days are 1–31. Always add 1 when displaying a month: new Date().getMonth() + 1. The Temporal API uses 1-indexed months.
Should I use a date library like date-fns or dayjs?
For complex date manipulation (parsing arbitrary formats, recurring events, calendar support) a library saves a lot of pain. date-fns is modular and tree-shakeable. dayjs is tiny and has a moment.js-compatible API. For basic formatting and arithmetic in modern environments, the native Date + Intl APIs are sufficient.
What is the difference between UTC and GMT?
For most practical purposes they are the same. GMT (Greenwich Mean Time) is a time zone. UTC (Coordinated Universal Time) is the international time standard that GMT is defined relative to. UTC never observes daylight saving time; some GMT-based time zones do. In code, always use UTC.
How to Convert an Epoch (Unix) Timestamp
A practical guide to Unix epoch timestamps — what they are, how to convert them to human-readable dates, and how to work with them in JavaScript, Python, and the command line.
Read guide →Understanding Timezones and UTC Offsets
A practical guide to timezones — understand UTC, IANA timezone names, UTC offsets, Daylight Saving Time, and how to reliably work with times across zones in code.
Read guide →