The Temporal API Is Finally Here: A Practical Guide to JavaScript's New Dates
Temporal is now shipping in Chrome and Firefox. A hands-on tour of PlainDate, ZonedDateTime, Instant and Duration — with the recipes you actually need day to day.
For as long as JavaScript has existed, Date has been its most reliably broken corner. Months are zero-indexed, everything is secretly mutable, parsing behavior varies between engines, and time zones are a guessing game. We have all shipped at least one off-by-one-day bug because of it — and then shipped a date library to apologize.
That era is finally ending. Temporal, the TC39 proposal that has been in the works for the better part of a decade, shipped in Firefox 139 and reached Chrome stable in version 144 earlier this year, with Safari's implementation underway in Technology Preview. It is a complete, immutable, time-zone-aware replacement for Date — built into the language, no library required. This post is a practical tour: the mental model, the recipes you will use weekly, and how to adopt it today without breaking older browsers.
The mental model: pick the right type
The single biggest idea in Temporal is that "a date" is not one thing. Date forced every concept — a birthday, a meeting time, a timestamp — into one object that was always secretly a millisecond count in UTC. Temporal splits these into distinct immutable types, and once you pick the right one, most bugs become unrepresentable:
Temporal.PlainDate— a calendar date with no time and no zone. Birthdays, invoices, deadlines.Temporal.PlainTime— a wall-clock time with no date. "The shop opens at 09:00."Temporal.PlainDateTime— date plus wall time, still zone-free. "The meeting is at 2pm on March 3rd" before you know where.Temporal.ZonedDateTime— a real moment anchored to a time zone. Calendar events, flight departures.Temporal.Instant— an exact point on the global timeline. Log timestamps,createdAtfields.Temporal.Duration— a span of time. "3 hours 20 minutes", "2 months".
Rule of thumb: store Instants, schedule with ZonedDateTime, and do business logic with PlainDate. If you find yourself reaching for PlainDateTime, ask whether you actually know the zone — you usually do.
Everyday recipes
Here is the stuff you do every week, side by side with the old pain. Today's date, without the new Date() timezone roulette:
const today = Temporal.Now.plainDateISO();
// => 2026-08-03 (a PlainDate, in the user's zone)
const inBerlin = Temporal.Now.plainDateISO("Europe/Berlin");
// today's date in a specific zone, explicitlyParsing is strict and predictable — ISO 8601 strings only, no more "works in Chrome, NaN in Safari":
const date = Temporal.PlainDate.from("2026-08-03");
const dt = Temporal.ZonedDateTime.from(
"2026-08-03T09:30:00+02:00[Europe/Berlin]"
);
date.year; // 2026
date.month; // 8 <- one-indexed. August is 8. Rejoice.
date.dayOfWeek; // 1 <- ISO: Monday is 1, Sunday is 7Arithmetic returns new objects — nothing mutates, and you never touch milliseconds math again:
const due = today.add({ days: 14 });
const lastMonth = today.subtract({ months: 1 });
// difference between two dates, in the unit you want
const days = today.until(due).days; // 14
const age = birthday.until(today, {
largestUnit: "years",
}); // => a Duration like P34Y6M12DComparison finally has a real API instead of subtracting objects and hoping:
Temporal.PlainDate.compare(a, b); // -1, 0, or 1
const sorted = dates.sort(Temporal.PlainDate.compare);
a.equals(b); // true / falseTime zones and DST, handled correctly by default
This is where Temporal earns its decade of design work. A ZonedDateTime knows its zone, so arithmetic respects daylight saving transitions instead of silently drifting an hour:
const zdt = Temporal.ZonedDateTime.from(
"2026-03-28T12:00:00+01:00[Europe/Berlin]"
);
// add one day across the DST switch (clocks jump forward)
zdt.add({ days: 1 }).toString();
// => 2026-03-29T12:00:00+02:00[Europe/Berlin]
// still noon on the wall clock - offset changed, not the hour
// add 24 exact hours instead - a different question!
zdt.add({ hours: 24 }).toString();
// => 2026-03-29T13:00:00+02:00[Europe/Berlin]Notice what happened: a day and 24 hours are different things near a DST boundary, and Temporal lets you say which one you mean. This is exactly the class of bug that used to surface twice a year as "the report is off by an hour, but only for European users, but only in March."
Converting between zones is explicit and lossless:
const meeting = Temporal.ZonedDateTime.from(
"2026-08-10T15:00:00+05:30[Asia/Kolkata]"
);
meeting.withTimeZone("America/New_York").toString();
// => 2026-08-10T05:30:00-04:00[America/New_York]Instants and timestamps
For machine time — logs, tokens, createdAt columns — use Temporal.Instant. It interoperates cleanly with everything you already have:
const now = Temporal.Now.instant();
now.toString(); // 2026-08-03T11:20:31.442Z
now.epochMilliseconds; // for APIs that want a number
// from a legacy Date
const inst = legacyDate.toTemporalInstant();
// render for a user, in their zone
inst.toZonedDateTimeISO("Asia/Kolkata").toPlainTime();That toTemporalInstant() method on Date.prototype is the official bridge: incremental migration is a first-class use case, not an afterthought. New code can be fully Temporal while old modules keep handing you Dates.
Using Temporal in production today
As of mid-2026 the support picture is: Firefox since 139, Chrome and Chromium-based browsers since 144, Safari in development (already visible in Technology Preview). Node has the proposal behind a flag in recent versions, and the @js-temporal/polyfill package covers everything else. That makes the pragmatic setup a feature-detected polyfill:
// temporal.js - import this everywhere instead of the global
import { Temporal as Polyfill } from "@js-temporal/polyfill";
export const Temporal = globalThis.Temporal ?? Polyfill;Browsers that ship Temporal natively pay zero bytes for the polyfill path once you code-split it, and everyone else gets identical behavior. When your browser support floor rises, you delete the file and change nothing else.
- Migrate the boundaries first: parse incoming strings into Temporal types immediately, and only convert to
Dateat the edge of libraries that require it. - Replace date-fns/dayjs call sites opportunistically — most one-liners (add, diff, startOf-style logic) map directly onto Temporal methods.
- Keep using
Intl.DateTimeFormatfor display; every Temporal type plugs intotoLocaleString()the way you would expect.
When you still want a library (or plain old Date)
Temporal is deliberately low-level in places: there is no "human friendly relative time" formatter (that is Intl.RelativeTimeFormat's job), no recurrence rules, and no calendar-week helpers beyond weekOfYear. Libraries will keep living above it, the way they now live above Intl. And if all a module does is stamp Date.now() into a log line, there is no prize for rewriting it.
But for everything that made dates in JavaScript miserable — parsing, zones, DST, immutability, arithmetic — the fix is now built into the platform. After nine years of proposal drafts, the boring answer is finally the right one: just use Temporal.