The Temporal API for Dates and Times
The Date object in JavaScript is a mess. It’s been that way since the language’s early days, and because of backward compatibility, nothing fundamental can change. The Temporal API fixes this by giving you a clean, modern set of types for working with dates and times. This guide walks through what Temporal offers and how to use it today.
Why the Date Object Is Broken
The Date object has design problems that can’t be patched:
- Mutable setters — Calling
date.setMonth()changes the object in place. If the same date object is referenced elsewhere in your code, you get surprising mutations. - Inconsistent parsing —
Date.parse()and theDateconstructor accept different string formats across browsers. - Timezone confusion — There’s no clean way to represent “a date and time without a timezone.” Everything is either UTC or the local device time.
- No calendar awareness — The
Dateobject only works with the Gregorian calendar. - No separate types — A
Dateobject mixes a point-in-time timestamp with calendar components. You can’t express “date only” or “time only” without workarounds.
These problems compound. Mutable setters cause bugs that are hard to track down. Timezone handling trips up every developer who works with dates seriously. Temporal solves all of this at the design level.
Getting Started with the Polyfill
Temporal is at Stage 3 in the TC39 process. Chrome shipped it in version 122, and Firefox support arrived in Firefox 139. For production today, the polyfill works in any environment:
npm install @js-temporal/polyfill
import { Temporal } from '@js-temporal/polyfill';
All Temporal types are immutable. Methods like add() and subtract() return new instances rather than modifying the original.
Core Types: Matching the Right Type to the Job
Temporal gives you distinct types for distinct concepts. Choosing the right one matters.
Temporal.Instant: A Point in Time
An Instant is an exact point on the global timeline, independent of timezone or calendar. Think of it as a nanosecond-precision timestamp. Use it for event logging, server timestamps, and anywhere you need an unambiguous ordering of moments.
const now = Temporal.Now.instant();
const instant = Temporal.Instant.fromEpochMilliseconds(Date.now());
const parsed = Temporal.Instant.from('2024-03-15T10:30:00Z');
Temporal.PlainDate: A Calendar Date
A PlainDate is just year, month, and day with no time and no timezone. Your birthday is a PlainDate. Holidays are PlainDate values. Use it for anything inherently calendar-based.
const birthday = Temporal.PlainDate.from({ year: 1990, month: 6, day: 15 });
const holiday = Temporal.PlainDate.from('2024-12-25');
Temporal.PlainTime: A Wall-Clock Time
A PlainTime is hour, minute, second, millisecond, and nanosecond with no date and no timezone. A daily alarm set to 7:00 AM is a PlainTime. The alarm fires at 7:00 AM regardless of what day it is or where you are.
const time = Temporal.PlainTime.from('09:45:30');
Temporal.PlainDateTime: Date and Time Without Timezone
A PlainDateTime combines a calendar date with a wall-clock time but carries no timezone information. This is exactly what you want when representing “what a user typed into a datetime picker” — before you know anything about their location.
const dt = Temporal.PlainDateTime.from('2024-12-25T08:00:00');
Temporal.ZonedDateTime: The Full Picture
A ZonedDateTime is a date, time, and timezone all together. This is the type to reach for when you need to display or manipulate a datetime as a human in a specific location would experience it.
const zdt = Temporal.ZonedDateTime.from({
year: 2024,
month: 3,
day: 10,
hour: 14,
minute: 30,
second: 0,
timeZone: 'America/New_York',
});
const nowInTokyo = Temporal.Now.zonedDateTimeISO('Asia/Tokyo');
Temporal.Duration: A Length of Time
A Duration represents a span of time with separate values for each unit (days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). Durations are not calendar-aware — they’re just quantities.
const dur = Temporal.Duration.from({ days: 3, hours: 12, minutes: 30 });
The Most Important Distinction: PlainDateTime vs ZonedDateTime
This trips up almost everyone coming from other date libraries. The difference is fundamental:
- A
PlainDateTimehas no timezone. It represents “March 10 at 2pm” without specifying which March 10 or which 2pm. - A
ZonedDateTimehas a timezone. “March 10 at 2pm in New York” is a specific instant.
Converting the same PlainDateTime to two different timezones produces two different instants:
const userInput = Temporal.PlainDateTime.from('2024-03-10T14:00:00');
const newYork = userInput.toZonedDateTime('America/New_York');
// → 2024-03-10T14:00:00[America/New_York]
const tokyo = userInput.toZonedDateTime('Asia/Tokyo');
// → 2024-03-10T14:00:00[Asia/Tokyo]
// Same wall-clock time, but 14 hours earlier on the global timeline
This matters when processing user input. A form field that says “March 10 at 2pm” doesn’t become a global timestamp until you apply a timezone.
Converting Between Types
Temporal types convert to each other through a predictable set of methods:
// PlainDate + PlainTime → PlainDateTime
const date = Temporal.PlainDate.from('2024-06-15');
const time = Temporal.PlainTime.from('10:30:00');
const dt = date.toPlainDateTime(time);
// PlainDateTime → ZonedDateTime (attach a timezone)
const zdt = dt.toZonedDateTime('America/New_York');
// ZonedDateTime → PlainDateTime (strip timezone)
const plain = zdt.toPlainDateTime();
// ZonedDateTime → Instant (convert to UTC point-in-time)
const instant = zdt.toInstant();
// Instant → ZonedDateTime (interpret in a timezone)
const zdtFromInstant = instant.toZonedDateTimeISO('Europe/London');
The toZonedDateTimeISO() method uses the ISO 8601 calendar automatically.
Arithmetic: Adding and Subtracting Time
Temporal makes date arithmetic straightforward and predictable.
const start = Temporal.PlainDate.from('2024-01-01');
const duration = Temporal.Duration.from({ months: 6, days: 15 });
const end = start.add(duration);
// → 2024-07-16
const back = start.subtract(duration);
// → 2023-06-17
For durations on an Instant, Temporal handles UTC implicitly:
const now = Temporal.Now.instant();
const later = now.add(Temporal.Duration.from({ hours: 2 }));
For calendar-aware arithmetic that accounts for DST transitions, convert to ZonedDateTime first:
const zdt = Temporal.Now.zonedDateTimeISO('America/New_York');
const dstAware = zdt.add({ hours: 2 }); // correctly handles DST transitions
Comparing two datetimes uses a static compare() method:
const cmp = Temporal.PlainDate.compare(
Temporal.PlainDate.from('2024-01-01'),
Temporal.PlainDate.from('2024-01-02')
);
// → -1 (first date is earlier)
if (cmp > 0) {
console.log('date1 is later');
}
Formatting Output
Temporal has no built-in formatter. For display, use the Intl.DateTimeFormat API or call toLocaleString() directly on any Temporal type:
const zdt = Temporal.Now.zonedDateTimeISO('America/New_York');
console.log(zdt.toLocaleString('en-US', { timeZone: 'America/New_York' }));
// → 4/2/2026, 2:30:00 AM
console.log(zdt.toLocaleString('en-GB', { timeZone: 'Europe/London' }));
// → 02/04/2026, 07:30:00
For more control over formatting, pair with Intl.DateTimeFormat:
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
});
const output = formatter.format(zdt.toInstant().toDate());
Note that toDate() converts to a native Date for Intl compatibility.
Non-Gregorian Calendars
Temporal supports alternative calendar systems through the calendar option:
const hebrewDate = Temporal.PlainDate.from({
year: 5784,
month: 9,
day: 15,
calendar: 'hebrew',
});
// Convert to Gregorian
const gregor = hebrewDate.toPlainDate({ calendar: 'iso8601' });
// → 2024-03-24
Other available calendars include japanese, chinese, islamic, persian, and indian, among others.
Common Pitfalls and How to Avoid Them
Duration addition to an Instant without timezone context is UTC-based. If you add { hours: 8 } to an Instant, it adds 8 hours in UTC. If you want DST-aware arithmetic, use ZonedDateTime instead.
PlainDate arithmetic with months is calendar-aware. Adding one month to January 31 produces either February 28 or February 29, depending on the year. This is correct — it respects the calendar.
Duration and Date math are different operations. A Duration represents a quantity of time, not a point. Comparing Duration objects to each other uses different semantics than comparing date objects.
See Also
The JavaScript Intl API handles locale-sensitive formatting for dates, numbers, and text. If you need to format Temporal output for specific locales, the Intl API is the companion to use alongside Temporal’s date logic.
JavaScript Promises are essential for handling asynchronous operations, which often go hand-in-hand with time-based logic like timeouts, retries, and scheduling.