Temporal reached TC39 Stage 4 on March 11. It's in the ES2026 spec. Chrome 144 shipped it in January. Firefox has had it since version 139. Edge gets it for free via Chromium.

After nine years of proposals, revisions, and at least three complete API redesigns, JavaScript finally has a date/time API that doesn't make you want to close your laptop.

Here's what that actually means for your codebase — pattern by pattern, with the npm uninstall commands you've been waiting to run.

The browser support situation right now

Chrome 144+, Firefox 139+, Edge 144+ all ship Temporal natively. No flags, no origin trials. Safari is still cooking — partial implementation in Technology Preview behind --use-temporal, full support expected late 2026. If you need Safari today, grab a polyfill.

# lightweight option (~20KB gzipped)
npm install temporal-polyfill

# full reference implementation (~44KB gzipped)
npm install @js-temporal/polyfill

For comparison, moment is 72KB gzipped and moment-timezone adds another 35KB on top. Even the polyfill is cheaper than what most of us are shipping today.

Timezone conversion without the baggage

The old way required pulling in an entire timezone database as a JavaScript bundle — 35KB extra just to answer "what time is it in Tokyo?"

// moment-timezone — 35KB extra for this
import moment from 'moment-timezone';
const ny = moment.tz('2026-03-15 14:00', 'America/New_York');
const tokyo = ny.clone().tz('Asia/Tokyo');
console.log(tokyo.format()); // 2026-03-16T04:00:00+09:00
// Temporal — zero dependencies, zero extra bytes
const ny = Temporal.ZonedDateTime.from({
  year: 2026, month: 3, day: 15, hour: 14,
  timeZone: 'America/New_York'
});
const tokyo = ny.withTimeZone('Asia/Tokyo');
console.log(tokyo.toString());
// 2026-03-16T04:00:00+09:00[Asia/Tokyo]

Notice the output includes the IANA timezone name in brackets. No more guessing what offset +09:00 refers to. This matters in regions that share the same UTC offset but observe different DST rules — Asia/Tokyo and Asia/Seoul are both +09:00 but historically diverged on summer time. Temporal keeps that distinction explicit.

The real win here isn't syntax. It's that the browser already has the IANA timezone database baked in. You were shipping a 35KB duplicate of data the runtime already knew. Temporal just lets you access it.

The small fixes that prevent real bugs

Some of Temporal's improvements are tiny in code but massive in impact. Month indexing is the classic example — every JavaScript developer has been bitten by new Date(2026, 2, 15) producing March 15 instead of February. Temporal.PlainDate.from({ year: 2026, month: 3, day: 15 }) means March, because months are 1-12, like a calendar. Pass month: 13 and it throws.

Validation is the same story. new Date('2026-02-29') silently gives you March 1. Temporal.PlainDate.from('2026-02-29') throws a RangeError telling you 2026 is not a leap year. Fail loud, fail fast.

Immutable arithmetic and DST safety

Moment's biggest trap was mutation. You'd pass a moment object to a utility function, it would call .add(), and your original variable was now a different date. This alone probably caused hundreds of production incidents per year across the ecosystem.

// moment — mutation footgun
const meeting = moment('2026-03-29');
const reminder = meeting.subtract(1, 'day');
console.log(meeting.format('YYYY-MM-DD'));
// '2026-03-28' — meeting got mutated too
// Temporal — every operation returns a new object
const meeting = Temporal.PlainDate.from('2026-03-29');
const reminder = meeting.subtract({ days: 1 });
console.log(meeting.toString()); // '2026-03-29' — untouched
console.log(reminder.toString()); // '2026-03-28'

Daylight saving transitions are where date bugs go to breed. The old Date object gives you no tools to handle the ambiguity — you get a result and you hope it's right. Temporal makes you choose explicitly:

Temporal.ZonedDateTime.from({
  year: 2026, month: 3, day: 8, hour: 2, minute: 30,
  timeZone: 'America/New_York'
}, { disambiguation: 'reject' });
// RangeError — 2:30 AM doesn't exist (clocks spring forward)

You can pass 'earlier', 'later', or 'compatible' (the default, which mimics legacy behavior). The point is you're making a choice instead of getting a surprise. For scheduling systems, cron jobs, or anything that runs "at 2:00 AM every day," this distinction is the difference between a silent misfire and a handled edge case.

Duration math gets the same treatment. start.until(end, { largestUnit: 'month' }) returns an ISO 8601 duration string like P2M14D — 2 months, 14 days. No manual division by 86400000, no approximate "month = 30 days" hacks.

Picking the right type

This is where Temporal diverges from being a better Date into being a genuinely different tool. It gives you types that match your domain:

  • Temporal.PlainDate — a birthday, a holiday. No time, no zone.

  • Temporal.PlainTime — "the meeting is at 14:00." No date.

  • Temporal.PlainDateTime — a calendar event before you know which timezone.

  • Temporal.ZonedDateTime — an exact moment in a specific place.

  • Temporal.Instant — a point on the UTC timeline. Timestamps, logs, database records.

  • Temporal.Duration — "3 hours and 20 minutes."

Stop storing birthdays as 2026-03-29T00:00:00.000Z and then wondering why it shows as March 28 for users west of Greenwich.

The migration path

You don't have to rip out moment or date-fns tomorrow. The practical approach:

Step 1 — Stop adding new Date or moment usage. Any new code uses Temporal (or the polyfill).

Step 2 — Bridge existing code where Date objects cross boundaries:

// Date → Temporal.Instant
const legacy = new Date();
const instant = Temporal.Instant.fromEpochMilliseconds(legacy.getTime());

// Temporal.Instant → Date
const backToDate = new Date(instant.epochMilliseconds);

Step 3 — When you touch a file for other reasons, migrate its date handling. Don't do a big-bang rewrite.

Step 4 — Once Safari ships native support (late 2026), drop the polyfill and run:

npm uninstall moment moment-timezone date-fns @js-temporal/polyfill

That's somewhere between 50KB and 110KB off your bundle, depending on what you're currently shipping.

The bottom line

Nine years. Three major API redesigns. A spec document longer than most novels. Bloomberg engineers contributing thousands of hours of implementation work. And now it's just... there, in your browser's global scope, waiting.

Temporal doesn't do anything revolutionary. It does what Date should have done in 1995. But it does it correctly, and after three decades of workarounds, correctly is revolutionary enough.