Topics

Jump to a topic.

Search questions

Find a question by title, tag, or summary.

What is Temporal, and which type do you use when?

Questions in this chapter (4)
  • Draft
  • medium
  • Askedsometimes
  • Verified October 2026

Answer

Temporal is a global namespace of immutable types that fix Date's problems: nanosecond precision, explicit time zones and calendars, no mutation, strict ISO 8601 / RFC 9557 parsing, and real date arithmetic. It reached stage 4 in March 2026 (the spec edition targeted is ES2027). Shipping: Firefox 139, Chrome and Edge 144, Node.js 26, Deno 2.7, Bun 1.4; Safari is still in preview. Elsewhere use @js-temporal/polyfill and feature-detect globalThis.Temporal.

Choose the type by what information you have:

Type Holds Use for
Instant an exact moment (epoch nanoseconds), no zone timestamps, logs, storage, ordering events
ZonedDateTime instant + time zone + calendar a moment in a place: meetings, flights, "9:00 in New York"
PlainDateTime date and time, no zone local wall-clock time not yet tied to a place
PlainDate calendar date birthdays, holidays, deadlines by day
PlainTime clock time alarms, opening hours
PlainYearMonth / PlainMonthDay partial date billing months, anniversaries
Duration years … nanoseconds an amount of time, not a position on the timeline

Rules that make the API safe:

  • Every object is immutable: add, with, round return new objects.
  • Calendar days and exact time are different units. On a ZonedDateTime, add({ days: 1 }) keeps the clock time across a DST change; add({ hours: 24 }) adds elapsed time.
  • Month-end overflow is a choice: overflow: 'constrain' (default) clamps, 'reject' throws.
  • Parsing is strict. Instant.from needs an offset or Z; ZonedDateTime.from needs a [Zone] annotation.
  • Compare with compare() / equals(); < and + throw instead of coercing.

Example

// Same output natively in Chrome 155 and with @js-temporal/polyfill 0.5.1 on Node 24.19.0 (no Temporal global)
import { Temporal } from '@js-temporal/polyfill';

// Instant: an exact moment, no zone, no calendar
const launch = Temporal.Instant.from('2026-03-08T12:00:00Z');
console.log(launch.toZonedDateTimeISO('Asia/Kolkata').toString());
// → 2026-03-08T17:30:00+05:30[Asia/Kolkata]

// ZonedDateTime: calendar days follow the clock, hours follow the stopwatch
// (US clocks moved forward on 2026-03-08)
const meeting = Temporal.ZonedDateTime.from('2026-03-07T09:00[America/New_York]');
console.log(meeting.add({ days: 1 }).toString());
// → 2026-03-08T09:00:00-04:00[America/New_York]
console.log(meeting.add({ hours: 24 }).toString());
// → 2026-03-08T10:00:00-04:00[America/New_York]

// PlainDate: no zone, so no DST; month overflow is explicit
const jan31 = Temporal.PlainDate.from('2026-01-31');
console.log(jan31.add({ months: 1 }).toString());
// → 2026-02-28
try { jan31.add({ months: 1 }, { overflow: 'reject' }); } catch (e) { console.log(e.name); }
// → RangeError

// Duration: a length of time, and every object is immutable
const span = jan31.until('2026-12-25', { largestUnit: 'month' });
console.log(span.toString(), jan31.with({ day: 1 }).toString(), jan31.toString());
// → P10M25D 2026-01-01 2026-01-31

Pitfalls

  • Using PlainDateTime for something that happened at a moment. It has no offset, so two systems can read it as different instants. Store an Instant, or a ZonedDateTime when the zone matters.
  • Adding { hours: 24 } when you mean "tomorrow at the same time", or the reverse. They differ on the two DST days of the year.
  • Assuming Temporal exists. Node 22 and 24 have no Temporal global, and Safari has not shipped it. A polyfill adds bundle size, so feature-detect before loading it.
  • Calling Duration.total() or round() in months or years without relativeTo. A month has no fixed length, so it throws.

Follow-ups

Why is there no Temporal.Date?
The names say what is missing: PlainDate has no time zone and no time; ZonedDateTime has both. There is no "date-time with an unknown zone" type that pretends to be a moment.
How do you convert a Date to Temporal?
date.toTemporalInstant() (defined by the spec on Date.prototype), then .toZonedDateTimeISO(zone) for a wall-clock view. The reverse is new Date(instant.epochMilliseconds), which truncates to milliseconds.
What happened to Temporal.TimeZone and Temporal.Calendar?
They were removed from the proposal before it reached stage 4. Time zones and calendars are now plain string identifiers ('Asia/Tokyo', 'hebrew') passed as arguments.

Deep dive

How it works

A type is defined by which fields it stores. Instant holds one BigInt of epoch nanoseconds. PlainDate holds an ISO year, month and day plus a calendar id; PlainTime holds hour to nanosecond. ZonedDateTime holds epoch nanoseconds, a time zone id and a calendar id, and derives its date and time fields from them. That is why it is the only type that can answer "what is 24 hours later" and "what is the same wall time tomorrow" differently.

ZonedDateTime serializes to RFC 9557: 2026-03-07T09:00:00-05:00[America/New_York]. The offset records the moment; the bracketed id lets a reader re-apply the zone's rules later. Duration holds signed integer fields for each unit (all non-zero fields share one sign) and is not balanced on creation. Converting between calendar units and exact units needs a reference point:

import { Temporal } from '@js-temporal/polyfill';

const month = Temporal.Duration.from({ months: 1 });
console.log(month.total({ unit: 'day', relativeTo: '2026-02-01' }), month.total({ unit: 'day', relativeTo: '2026-03-01' }));
// → 28 31
try { month.total({ unit: 'day' }); } catch (e) { console.log(e.name); }
// → RangeError
console.log(Temporal.Duration.from({ hours: 25 }).round({ largestUnit: 'day' }).toString());
// → P1DT1H

Output above matches natively in Chrome 155 and with the polyfill on Node 24.19.0, like every Temporal output in this question.

Edge cases

  • Instant rejects calendar units: instant.add({ days: 1 }) throws a RangeError, because a day is not a fixed length. Use hours, or convert to ZonedDateTime.
  • PlainDate.from('2026-03-08T12:00:00Z') throws: a Z designator means an exact moment, which a plain value must not silently discard.
  • Formatting goes through toLocaleString(). new Intl.DateTimeFormat().format(zonedDateTime) throws a TypeError (the polyfill's message says to call toLocaleString()), and options that do not apply to the type are rejected (a timeStyle on a PlainDate). Both throw natively in Chrome 155 and in the polyfill; I did not read the matching ECMA-402 text.
  • toString() for Instant is always UTC (…Z); to show a zone, convert first.
  • Date.prototype.toTemporalInstant exists only where Temporal does; the polyfill exports it as a standalone function instead.

Real-world scenario

A monthly billing date anchored on the 31st. Chaining add({ months: 1 }) drifts, because each clamp loses the original day. Add the total offset to the anchor instead:

import { Temporal } from '@js-temporal/polyfill';

const anchor = Temporal.PlainDate.from('2026-01-31');
const chained = anchor.add({ months: 1 }).add({ months: 1 });
console.log(chained.toString(), anchor.add({ months: 2 }).toString());
// → 2026-03-28 2026-03-31
const bill = (n) => anchor.add({ months: n }).toString();
console.log([1, 2, 3].map(bill));
// → [ '2026-02-28', '2026-03-31', '2026-04-30' ]

With Date the same logic needs manual last-day-of-month checks and mutation of a copy.

Trade-offs

Temporal costs a larger API surface (eight types) and an explicit choice at every call site; Date lets you skip that choice and then breaks. Native support is new, so libraries that serve older engines still need a polyfill, and Date remains the type many platform APIs return or accept. For an Instant that only needs ordering and storage, a plain epoch number or ISO string is still enough.

How I checked support: typeof Temporal was undefined on Node 22.23.2, 24.19.0, and on a Homebrew build of Node 26.7.0, whose process.config.variables.v8_enable_temporal_support was 0 (Node's build docs say Temporal support requires a build with temporal_rs). Node 26's release notes say it is enabled by default, and MDN's compatibility data lists Node 26.0.0, so treat my local Node 26 as a build-configuration exception, not the norm.

Exercises

Same clock time, or same elapsed time?

import { Temporal } from '@js-temporal/polyfill';

const start = Temporal.ZonedDateTime.from('2026-11-01T00:30[America/New_York]');
console.log(start.add({ days: 1 }).toString());
console.log(start.add({ hours: 24 }).toString());