The JavaScript Temporal API: A Modern Replacement for Date
JavaScript Date has been broken in well-known ways for decades. TC39 Temporal — Stage 4 since early 2025, shipping in modern browsers and Node.js — cleanly separates instants, wall-clock dates, and zoned date-times. This guide explains the new types, how they map to Unix timestamps, and how to migrate.
Temporal vs Date at a glance
JavaScript's built-in Date has been a known source of bugs for decades: it is mutable, it only understands the local timezone and UTC, its months are zero-indexed, and its parsing is loosely specified. Temporal is the standards-track replacement — Stage 4 and part of ECMAScript 2026 — and fixes each of those problems with purpose-built, immutable types. The table below is the high-level contrast; the rest of this guide covers the types and how they map to Unix timestamps.
| Aspect | Date (legacy) | Temporal |
|---|---|---|
| Mutability | Mutable | Immutable |
| Precision | Milliseconds | Nanoseconds |
| Timezones | Local + UTC only | Any IANA zone (ZonedDateTime) |
| Month indexing | 0-based (0 = January) | 1-based (1 = January) |
| Parsing | Lenient, implementation-defined | Strict ISO 8601 / RFC 9557 |
| Instant vs wall-clock | Conflated in one type | Separate types |
Why Temporal exists
JavaScript Date conflates several distinct concepts: an exact instant, a wall-clock time, and a zoned date-time. Date also has well-known parsing quirks, a single timezone (the user’s), and millisecond precision. Temporal exposes the underlying concepts as separate types and supports nanosecond precision, arbitrary IANA timezones, and explicit calendar systems.
- Stage 4 since early 2025; first shipped in Firefox 139 by default
- Now available in Chrome 144+, Firefox 139+, and Node.js 26+
- Polyfills (@js-temporal/polyfill) work in any modern environment
- Designed to coexist with Date — interop methods exist in both directions
Core Temporal types
A small set of types covers the date-time problem cleanly. Each has a canonical string form (ISO 8601, extended by RFC 9557 for the zoned type):
| Type | Represents | Example string |
|---|---|---|
| Temporal.Instant | An exact instant, like a Unix timestamp (ns precision) | 2026-01-01T00:00:00Z |
| Temporal.ZonedDateTime | Instant + IANA timezone + calendar | 2026-01-01T09:00:00+09:00[Asia/Tokyo] |
| Temporal.PlainDateTime | Date + time, no timezone | 2026-01-01T09:00:00 |
| Temporal.PlainDate | A calendar date (e.g. a birthday) | 2026-01-01 |
| Temporal.PlainTime | A wall-clock time | 09:00:00 |
| Temporal.Duration | A length of time, unit-aware | P3DT4H10M |
Working with Unix timestamps
Temporal.Instant is the Unix-timestamp type. The shipped API exposes only millisecond and nanosecond epoch conversions — the earlier fromEpochSeconds and fromEpochMicroseconds methods were removed before Stage 4 — so convert Unix seconds by multiplying by 1000. Use Instant for storage and transport, and ZonedDateTime when you need a wall-clock view in a specific timezone.
- Temporal.Now.instant() — the current exact moment
- Temporal.Instant.fromEpochMilliseconds(Date.now()) — from a JS millisecond timestamp
- Temporal.Instant.fromEpochMilliseconds(1700000000 * 1000) — from Unix seconds (× 1000)
- Temporal.Instant.fromEpochNanoseconds(1700000000000000000n) — from nanoseconds (BigInt)
- instant.epochMilliseconds and instant.epochNanoseconds — read the epoch value back
- instant.toZonedDateTimeISO("America/New_York") — a wall-clock view in a zone
Timezones and DST handled correctly
ZonedDateTime carries an IANA timezone, so DST transitions, gap times, and overlap times are all handled explicitly. The disambiguation option lets you pick a policy for ambiguous local times, and the canonical string format is RFC 9557 (an ISO 8601 timestamp with a bracketed zone).
- zdt.timeZoneId — the IANA name (e.g. America/Los_Angeles)
- zdt.add({ hours: 1 }) — DST-aware arithmetic in wall-clock terms
- zdt.until(zdt2) — distance between two zoned date-times
- Disambiguation options: "earlier", "later", "compatible" (default — later for gap, earlier for overlap), "reject"
- Round-trip: instant ↔ zoned date-time is loss-free
Migration patterns from Date
The simplest migration strategy is to introduce Temporal at the boundaries first (parsing input, formatting output) and gradually move internal logic over.
- Date → Instant: Temporal.Instant.fromEpochMilliseconds(date.getTime())
- Instant → Date: new Date(instant.epochMilliseconds)
- For new code, prefer Temporal everywhere and convert to Date only at API edges
- date-fns and Luxon both have Temporal-aware modes / adapters
- JSON: Temporal.Instant.prototype.toJSON returns an ISO 8601 string
Browser and runtime support
Temporal shipped in 2025 and native support is now broad, but not yet universal. The polyfill is the safest path while older browsers and Safari stable are still in service.
- Firefox 139+: first to ship, enabled by default (May 2025)
- Chrome 144+: enabled by default (January 2026)
- Node.js 26+: enabled by default (May 2026)
- Safari: available in Technology Preview, not yet in a stable release
- Polyfill: @js-temporal/polyfill or temporal-polyfill — same API, any modern environment
Related references
FAQ
- Is Temporal a replacement for Date?
- Yes — Temporal is designed to fully replace Date for new code. Date remains in the language for backward compatibility, and the two interoperate via epoch milliseconds.
- How do I convert a Unix timestamp to Temporal?
- Use Temporal.Instant.fromEpochMilliseconds(). For Unix seconds, multiply by 1000: Temporal.Instant.fromEpochMilliseconds(seconds * 1000). The shipped API only has fromEpochMilliseconds and fromEpochNanoseconds — the older fromEpochSeconds method was removed before Temporal reached Stage 4.
- Does Temporal support nanoseconds?
- Yes. Temporal.Instant has nanosecond precision internally and exposes epochNanoseconds as a BigInt.
- Can I use Temporal in Node.js today?
- In Node.js 26+ Temporal is enabled by default. In older Node versions, install @js-temporal/polyfill — the API surface is identical.
- Which browsers support Temporal?
- Firefox 139+ (May 2025), Chrome 144+ (January 2026), and Node.js 26+ (May 2026) ship Temporal by default. Safari has it in Technology Preview. For anything older, use the @js-temporal/polyfill.
- How does Temporal handle the DST gap?
- ZonedDateTime constructors and arithmetic accept a disambiguation option. "compatible" (the default) picks the later instant for gaps and the earlier for overlaps; "reject" throws an error if the local time is ambiguous.