The JavaScript Temporal API is available now in Chrome 144, Firefox 139 and Node.js 26, and the proposal has reached Stage 4 at TC39. Safari has not shipped it in a stable release, so browser code still needs a polyfill as of August 2026. On the server you can use it natively today, with nothing installed.
That split decides most of the question for you. Node code adopts Temporal for free. Front-end code pays around 20 kB until Apple ships.
Where can I use the JavaScript Temporal API today?
Firefox got there first, in Firefox 139 on 27 May 2025. Chrome followed in version 144 on 13 January 2026. Node.js 26 enabled Temporal by default on 5 May 2026, riding V8 14.6, and that line enters long-term support in October 2026. If you are on Node 24 you need --harmony-temporal, which is a reasonable reason to wait for the LTS.
Safari is the holdout that matters. WebKit added Temporal in Safari Technology Preview 249 and has been grinding through edge cases since, including calendar era fields and duration rounding, but it is not in a shipping Safari yet. MDN marks Temporal as limited availability rather than Baseline for exactly that reason.
Two answers, then. In an API, a worker, a build script or a cron job, use it directly. In anything that runs in a browser, add temporal-polyfill from the FullCalendar team, which describes itself as “less than 20 kB”, or @js-temporal/polyfill from the proposal champions. The FullCalendar package has a global entrypoint:
import 'temporal-polyfill/global'
It also exposes a tree-shakeable function API under temporal-polyfill/fns/*, which is worth the awkwardness if you only need two operations and 20 kB is real money on your bundle budget.
What does Temporal fix that Date never did?
Date is one type doing five jobs. A birthday, an invoice due date, a meeting in Sydney and a log line all become milliseconds since the epoch, interpreted through whatever time zone the machine happens to be set to. That single representation is why new Date('2026-09-30') and new Date('2026-09-30T00:00') land on different instants, and why almost every codebase grows a startOfDay helper that quietly assumes the server runs in UTC.
Temporal splits the job into separate types that refuse to pretend they are each other.
// A calendar date. No time, no zone. An invoice date, a birthday.
const due = Temporal.PlainDate.from('2026-09-30');
due.add({ months: 1 }).toString(); // '2026-10-30'
// A wall clock in a real place. A meeting, a delivery slot.
const call = Temporal.ZonedDateTime.from('2026-09-30T14:00:00[Europe/London]');
// A point on the timeline. A log entry, an audit row.
const seen = Temporal.Now.instant();
Nothing converts implicitly. Turning a PlainDate into an exact instant means naming a time zone, and that is the whole design. The bugs Temporal removes are the ones where a conversion happened without anybody deciding it should.
The string format is RFC 9557: an ISO 8601 timestamp with the IANA zone in square brackets, so 2026-09-30T14:00:00+01:00[Europe/London] round-trips through toString() and from() without losing the zone. That alone fixes a category of bug where a timestamp travels through JSON and comes back as UTC.
Which Temporal type do I actually need?
Pick by asking what would still be true if the reader moved country.
Use PlainDate when the answer is “the same day, everywhere”: invoice dates, contract start dates, birthdays. Use ZonedDateTime when a person will look at a clock on a wall: meetings, opening hours, booking slots. Use Instant when the machine cares and the human does not, which covers most created_at columns and every log line. PlainTime, PlainYearMonth and PlainMonthDay cover recurring things, so “every weekday at 09:00”, a card expiry and “25 December” each get a type that cannot accidentally acquire a year or a zone.
Storage follows from the type. An Instant goes in a column as a UTC timestamp and nothing is lost. A future ZonedDateTime should be stored as the local date-time plus the IANA identifier, in two columns, because a government can change the offset between now and the appointment. Storing that as UTC bakes in an offset that may not be true when the day arrives. If you are keeping this in SQLite, our write-up on choosing between node:sqlite and better-sqlite3 covers the driver side of the same decision.
How does Temporal handle daylight saving?
British clocks go back at 02:00 on 25 October 2026, so 01:30 that morning happens twice. Date cannot tell you which one you meant. Temporal makes the ambiguity explicit and applies a documented default if you say nothing.
const ambiguous = '2026-10-25T01:30:00[Europe/London]';
Temporal.ZonedDateTime.from(ambiguous).offset;
// '+01:00' BST, the earlier of the two
Temporal.ZonedDateTime.from(ambiguous, { disambiguation: 'later' }).offset;
// '+00:00' GMT, after the clocks change
Temporal.ZonedDateTime.from(ambiguous, { disambiguation: 'reject' });
// RangeError
Temporal.ZonedDateTime.from('2026-10-25T12:00[Europe/London]').hoursInDay;
// 25
The default, compatible, takes the earlier instant for a repeated time and skips forward across a gap, which matches what Date does. Reach for reject when the value came from a user and an error beats a plausible wrong answer.
A second option, offset, handles strings that carry both an explicit offset and a zone and disagree with themselves. reject is the default for from() and prefer for with(). Choose use when you are replaying a historical event and the exact instant is what you must preserve, and ignore when the local wall time is the thing that matters.
How do I migrate a codebase that already uses Date?
Convert at the boundary rather than everywhere. Date.prototype.toTemporalInstant() is the bridge in, and the epoch milliseconds are the bridge back out.
const legacy = new Date('2026-08-29T09:15:00Z');
const instant = legacy.toTemporalInstant();
instant.toZonedDateTimeISO('Europe/London').toPlainDate().toString();
// '2026-08-29'
// Back out again, for an API that still wants a Date
const back = new Date(instant.epochMilliseconds);
Two things will bite on the first attempt.
valueOf() throws a TypeError on every Temporal object, on purpose, so a > b stops compiling into a silent numeric comparison. Use the static comparator instead, which returns -1, 0 or 1 and works directly as a sort function:
const dates = ['2026-12-25', '2026-08-29', '2026-10-25'].map(Temporal.PlainDate.from);
dates.sort(Temporal.PlainDate.compare);
const gap = dates[0].until(dates[2], { largestUnit: 'month' });
gap.toString(); // 'P3M26D'
gap.total({ unit: 'day', relativeTo: dates[0] }); // 118
And toTemporalInstant() throws a RangeError on an invalid Date where the old code produced NaN and carried on for another six functions. That is an improvement, but it moves the failure to a place you have not written a handler for. Validate date strings as they arrive rather than catching further in, which is a job for whatever schema library you already run at the edge of your request handlers, as in our note on validation without adapter packages.
Should you adopt it?
On Node 26, yes, for any new date logic you write. Convert files as you touch them and let the date-fns import disappear over a few months rather than booking a refactor. The types pay for themselves the first time somebody has to reason about a booking that crosses a clock change.
In the browser, adopt it when dates are the product. Scheduling, calendars, availability, anything where a wrong offset is a support ticket, is worth 20 kB and the polyfill is stable. It is the kind of trade we make routinely on booking and scheduling builds. What is not worth it is polyfilling Temporal to render “posted 3 days ago”, which Intl.RelativeTimeFormat already does, or to handle UTC timestamps that never touch a time zone at all. Date is bad at nine things and adequate at that one.