•5 min read

JavaScript Temporal: How to Replace Date in 2026

The clocks changed last weekend and, right on schedule, a reminder in an app I maintain fired an hour early for some of our users. I opened the file responsible, which in every JavaScript codebase I have ever worked on is called dateUtils.ts, and found the usual pile of workarounds: a hand-rolled addMonths, a comment explaining why someone subtracted 1 from a month, and a helper that exists only because Date cannot say "9am in Tokyo".

Here is the thing though. The reason that file exists just went away. Temporal reached Stage 4 at TC39's March 2026 meeting, so the API is finished, and it is already running by default in Firefox, Chrome, Edge, and Node.js 26. I have been moving small pieces of real projects onto it for a few weeks. The honest version follows.

Three desk clocks set to different time zones next to a tablet and a notebook

Why Date kept breaking things

The classic one, which I have shipped twice in my career:

const d = new Date(2026, 0, 31)  // January 31
d.setMonth(1)                     // "February 31" overflows to March 3

Months are zero-indexed, the object is mutable, parsing is inconsistent across engines, and expressing "an event at 9am in a specific place" means dragging in a library. That last point is why date-fns and Luxon exist, and why every developer has a bug story that only happens twice a year. Mine fired this Sunday.

Temporal fixes all of it, not with a better Date, but by not trying to be one type that does everything. It gives you separate types for separate concepts, and picking the right one does most of the work.

One type per concept

Instant is an exact moment on the timeline. PlainDate is a calendar date, no time attached. PlainDateTime is wall-clock date and time with no zone. ZonedDateTime is an instant plus a named IANA zone, and that is the one for anything that corresponds to a real event. Every value is immutable, so a utility function can no longer reach into your date and quietly change it.

Arithmetic behaves the way you meant it to. Adding a month to January 31 clamps to February 28 instead of spilling into March, and overflow: "reject" turns the clamp into an error:

const due = Temporal.PlainDate.from("2026-01-31");

due.add({ months: 1 }).toString();                        // "2026-02-28"
due.add({ months: 1, overflow: "reject" });               // throws
due.until(Temporal.PlainDate.from("2026-11-06")).days;    // 279

Months are one-based now. That sounds like a footnote, but I spent over a decade mentally subtracting one from every getMonth() call, so it took a while to stop.

DST is where it earns its keep

This is the example I keep pasting into code review. It is the beginning of March in New York, right before the spring-forward night:

const alarm = Temporal.ZonedDateTime.from("2026-03-07T09:00:00[America/New_York]");

alarm.add({ days: 1 }).toString();   // 2026-03-08T09:00:00-04:00, still 9am
alarm.add({ hours: 24 }).toString(); // 2026-03-08T10:00:00-04:00, 10am

Adding one calendar day keeps the wall-clock time through the transition. Adding 24 hours does not. Plain Date cannot even express that distinction, and it is the difference between a reminder firing at 9am and one firing at 10am after the clocks change.

Which is to say, the bug I was looking at on Sunday.

Using it today

Support right now: Firefox 139 and later (there since May 2025), Chrome and Edge 144 (January 2026), and Node.js 26 (May 2026, no flag). Deno has it too. Safari is the holdout: the API lives only in Safari Technology Preview so far. WebKit says default-on support is coming later this year, but I stopped betting deadlines on browser timelines.

On the server it is just there if you run Node 26. In the browser, feature-detect and polyfill:

if (typeof Temporal === "undefined") {
  const { Temporal } = await import("temporal-polyfill");
  globalThis.Temporal = Temporal;
}

There are two polyfills. temporal-polyfill from the FullCalendar team is the smaller one with stable releases, and @js-temporal/polyfill is the reference implementation, closer to the spec but heavier. I picked the small one for client bundles. Polyfilling a finished standard has one nice property: when Safari finally ships, you delete the import and nothing else changes.

Migrating without a rewrite

I did not convert the whole codebase, and you should not either. date-fns still works and Luxon is not leaving our backend this quarter. The pattern that has held up is converting at the boundary: when a date enters from an API or a database row, turn it into a Temporal type immediately, do all the reasoning there, and convert back only where an old API insists:

// legacy Date in, Temporal for the logic
const instant = new Date(row.created_at).toTemporalInstant();
const local = instant.toZonedDateTimeISO("Asia/Shanghai");

// and back out, when some old function demands a Date
const legacy = new Date(instant.epochMilliseconds);

Formatting needs nothing new either. Intl.DateTimeFormat accepts Temporal objects directly, so the display side keeps working while the logic side improves.

Small things worth knowing

valueOf throws on purpose, so sloppy coercion and == comparisons fail loudly instead of silently comparing garbage. Use equals() for equality and Temporal.PlainDate.compare, which doubles as a sort function. Those two choices felt annoying for a day and then felt correct.

And one thing I got wrong in my first week: a Duration does not rebalance itself. a.until(b) hands you a raw duration, and you call round() or total() to get what you actually meant. I assumed it would normalize and shipped a log line that said "hours: 297". Nothing broke, but the monitoring graph looked very dramatic.

I am not going to pretend this deletes every date library by Christmas. Safari has not shipped and the polyfill costs real kilobytes. But new code I write is Temporal, and for the first time in a long JavaScript career I can picture dateUtils.ts not existing. The platform has been taking its jobs back, the same way native CSS did.

My migration notes and that boundary helper live in Snippet Ark, next to the config snippets I collected when I upgraded to Node 26 and found Temporal already waiting. If you keep a list of things the platform finally fixed, this one goes near the top.