The JavaScript Date Object's Timezone Problem
JavaScript's Date object always stores time as a UTC timestamp internally, but most of its methods return values in the browser's local timezone. This inconsistency causes many subtle bugs, especially when code runs in different environments.
const d = new Date("2024-03-01T14:00:00Z"); // UTC
console.log(d.getHours()); // Local hours — varies by browser location!
console.log(d.getUTCHours()); // 14 — always UTC, predictable
Using the Intl API (Built-in, No Library Needed)
The modern Intl.DateTimeFormat API lets you format dates in any timezone without external dependencies:
const d = new Date("2024-03-01T14:00:00Z");
// Format in Seoul time
const seoulFormatter = new Intl.DateTimeFormat("ko-KR", {
timeZone: "Asia/Seoul",
dateStyle: "full",
timeStyle: "short",
});
console.log(seoulFormatter.format(d));
// 2024년 3월 1일 금요일 오후 11:00
// Format in New York time
const nyFormatter = new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
hour: "2-digit",
minute: "2-digit",
timeZoneName: "short",
});
console.log(nyFormatter.format(d));
// 09:00 AM EST
Luxon: The Modern Date Library
Luxon is the spiritual successor to Moment.js, built for immutability and first-class timezone support via the Intl API:
import { DateTime } from "luxon";
// Create from UTC ISO string
const dt = DateTime.fromISO("2024-03-01T14:00:00Z");
// Convert to Seoul timezone
const seoul = dt.setZone("Asia/Seoul");
console.log(seoul.toString());
// 2024-03-01T23:00:00.000+09:00
// Get specific parts
console.log(seoul.hour); // 23
console.log(seoul.offset); // 540 (minutes)
// Format
console.log(seoul.toFormat("yyyy-MM-dd HH:mm z"));
// 2024-03-01 23:00 KST
Temporal: The Future (Stage 3 Proposal)
The Temporal API is a TC39 Stage 3 proposal that will replace Date with a comprehensive, timezone-aware date library built into JavaScript itself. It distinguishes between wall-clock time (PlainDateTime), exact time (Instant), and zoned time (ZonedDateTime):
// Future API (polyfill available now)
const instant = Temporal.Now.instant();
const zonedSeoul = instant.toZonedDateTimeISO("Asia/Seoul");
console.log(zonedSeoul.toString());
Best Practices
- Parse dates from APIs as UTC ISO strings:
new Date("2024-03-01T14:00:00Z") - Use
getUTC*methods instead ofget*for consistent server-side behavior (Node.js) - For complex timezone logic, use Luxon or date-fns-tz
- Never use
new Date("2024-03-01")without a time — it parses as midnight UTC, which may display as the previous day in western timezones