
JavaScript’s Temporal API Reached Stage 4: A Practical Guide to Replacing Date in Production
By Ghazi Khan | Sep 26, 2026 - 9 min read
In March 2026, TC39 advanced the Temporal API to Stage 4, which means it is now finished and part of the ECMAScript 2026 specification. That single line undersells how big a deal it is. Temporal is not a new method bolted onto Date, it is a full replacement built from scratch, because Date has been structurally broken since JavaScript shipped in 1995 and nobody could fix it without breaking the web. The proposal took almost nine years to reach this point, and it is now shipping natively in Chrome, Firefox, and Node.js 26, with TypeScript 6.0 shipping type definitions for it.
If you have ever fought a timezone bug the night before a release, or watched a date silently shift by a day because of daylight saving time, this post is for you. We will go through exactly what was wrong with Date, how Temporal's type system fixes each problem, and how to start migrating real code today, whether or not your target browsers have native support yet.
What Stage 4 actually means, and why the timing matters now
TC39, the committee that standardizes JavaScript, moves every proposal through five stages before it becomes part of the language.
Diagram100%flowchart LR S0["Stage 0: Strawman idea"] --> S1["Stage 1: Proposal accepted"] S1 --> S2["Stage 2: Draft spec text"] S2 --> S3["Stage 3: Candidate,\nspec complete,\nengines start experimenting"] S3 --> S4["Stage 4: Finished,\npart of ECMAScript 2026\n(March 2026)"] style S4 fill:#1f3a24,stroke:#4ab06a,color:#eeevisualized by
Stage 4 is the highest level a proposal reaches. It means the specification text is complete, at least two independent implementations pass the full test suite, and the feature is guaranteed to appear in the next yearly ECMAScript edition. It does not mean every browser ships it the day the stage changes, but it does mean engines stop treating it as experimental and start shipping it behind stable flags, then unflagged. That is exactly what has been happening around Temporal since the Stage 4 vote:
- Firefox shipped Temporal natively starting with Firefox 139 (May 2025).
- Chrome and Edge (both Chromium-based) shipped it natively starting with Chrome 144 (January 2026).
- Safari Technology Preview has most of the API implemented, with full stable Safari support expected to follow.
- Node.js 26.0.0, released May 5, 2026, ships Temporal enabled by default, no flag required. Node 26 becomes an LTS release in October 2026.
- TypeScript 6.0, released March 23, 2026, includes built-in type definitions for Temporal (available once you target
esnextor add it to yourlibarray).
If your runtime is not there yet, two polyfills fill the gap: @js-temporal/polyfill and temporal-polyfill, both implementing the full spec in JavaScript so you can start writing Temporal-based code before your minimum supported browser catches up.
The six things actually wrong with Date
To understand why this took nine years, you have to understand how deep the Date object's problems go. These are not style complaints, they are correctness bugs that ship to production.
Mutability. Every Date method that "changes" a date mutates the object in place and returns nothing useful. If two variables reference the same Date instance, and one gets adjusted, both change.
const meeting = new Date('2026-03-15T10:00:00');
const reminder = meeting;
reminder.setHours(reminder.getHours() - 1);
console.log(meeting.getHours()); // 9, not 10
// meeting and reminder were never two separate dates, they were
// two names for the same mutable object, and setHours mutated it.
Parsing unreliability. new Date(string) accepts an enormous range of formats, and how it interprets an ambiguous string like "03/04/2026" depends on the engine and the user's locale. There is no reliable way to parse a date string cross-browser without a library, which is exactly why moment.js and date-fns exist in the first place.
Timezone limitations. A Date object stores a single instant in time (milliseconds since the Unix epoch), and it can only render that instant in two ways: the runtime's local timezone, or UTC. There is no way to ask "what did this instant look like in Tokyo" while your code is running in a browser set to IST. Any app that needs to display or compute times in a timezone other than the user's own has to hand-roll timezone math or pull in a library.
DST handling. Because Date arithmetic operates on local wall-clock fields without understanding daylight saving transitions, adding "one day" across a DST boundary can silently produce a time that is off by an hour, and the exact behavior differs by engine.
Non-Gregorian calendar gaps. Date only understands the Gregorian calendar. Any app that needs to work with the Japanese, Islamic (Hijri), Buddhist, or Hebrew calendar, common requirements for apps built for global or specific regional audiences, has no native support at all.
Zero-indexed months. A smaller but famously error-prone quirk: getMonth() returns 0 for January and 11 for December, off by one from how every human reads a calendar. It is one of the most repeated bugs in JavaScript history, and it exists purely because the original Date implementation copied a decision from Java's java.util.Date in 1995.
Temporal's type system: one type per shape of "time"
Temporal replaces the single, overloaded Date object with several focused, immutable types. Every operation on a Temporal object returns a new object instead of mutating the original, which by itself eliminates the mutability bug category entirely.
Diagram100%flowchart TD A{What are you\nrepresenting?} -->|Just a calendar date, no time| B["Temporal.PlainDate"] A -->|Just a time of day, no date| C["Temporal.PlainTime"] A -->|Date and time, no timezone| D["Temporal.PlainDateTime"] A -->|Real wall-clock time in a place| E["Temporal.ZonedDateTime"] F{A precise machine\ntimestamp?} -->|Yes, for logging, sorting, comparing| G["Temporal.Instant"] H{An interval or\nelapsed time?} -->|Yes| I["Temporal.Duration"]visualized by
A few of these deserve concrete examples, since the whole point is that each type refuses to let you accidentally mix concerns that Date used to blur together.
Temporal.PlainDate is a calendar date with no time or timezone attached, correct for things like a birthday or a deadline that is the same date everywhere:
const deadline = Temporal.PlainDate.from('2026-12-01');
console.log(deadline.month); // 12, not 11. No zero-indexing.
console.log(deadline.dayOfWeek); // 1-7, ISO weekday number
Temporal.ZonedDateTime is the type that finally gives JavaScript a real answer to "what time is it right now in Mumbai, expressed correctly even across a DST change":
const mumbai = Temporal.Now.zonedDateTimeISO('Asia/Kolkata');
const newYork = mumbai.withTimeZone('America/New_York');
console.log(mumbai.toString()); // 2026-09-21T14:32:10+05:30[Asia/Kolkata]
console.log(newYork.toString()); // 2026-09-21T05:02:10-04:00[America/New_York]
// Same instant, two correct wall-clock representations, computed natively.
ZonedDateTime arithmetic is also DST-aware by construction, which is the fix for the fourth flaw above:
const nyTime = Temporal.ZonedDateTime.from(
'2026-03-08T01:30:00-05:00[America/New_York]',
);
const nextDay = nyTime.add({ days: 1 });
console.log(nextDay.toString());
// Wall-clock time is preserved correctly across the DST boundary,
// the UTC offset adjusts automatically instead of silently drifting an hour.
Temporal.Instant is a precise point on the universal timeline, with nanosecond precision instead of Date's millisecond ceiling, which matters for logging, event ordering, and anything performance-sensitive:
const now = Temporal.Now.instant();
console.log(now.epochMilliseconds); // same precision Date gave you
console.log(now.epochNanoseconds); // BigInt, nanosecond precision
Temporal.Duration replaces the manual millisecond math developers used to write for "how long until this event":
const start = Temporal.PlainDate.from('2026-01-15');
const end = Temporal.PlainDate.from('2026-09-25');
const gap = start.until(end, { largestUnit: 'months' });
console.log(gap.toString()); // "P8M10D" (8 months, 10 days, ISO 8601 duration format)
And comparisons use explicit static methods instead of relying on numeric coercion, which removes a whole class of date1 > date2 bugs where the comparison silently works for Date (because it coerces to a timestamp) but fails for plain strings:
const a = Temporal.PlainDate.from('2026-01-01');
const b = Temporal.PlainDate.from('2026-06-01');
Temporal.PlainDate.compare(a, b); // -1 (a is earlier)
Calendar support is built in too. Temporal.PlainDate.from({ year: 2026, month: 1, day: 1, calendar: 'islamic' }) works natively, no library required, which closes the fifth flaw for any app that needs to render dates in a non-Gregorian calendar.
Migrating real code without breaking everything at once
You do not need native support everywhere to start. The realistic path is interop: convert at the boundary between legacy Date-based code (APIs, older libraries, localStorage) and new Temporal-based logic.
Diagram100%sequenceDiagram participant API as Server response (epoch ms) participant Instant as Temporal.Instant participant Zoned as Temporal.ZonedDateTime participant UI as Formatted string for the user API->>Instant: Temporal.Instant.fromEpochMilliseconds(ms) Instant->>Zoned: instant.toZonedDateTimeISO("Asia/Kolkata") Zoned->>UI: zoned.toLocaleString("en-IN", { dateStyle: "full", timeStyle: "short" }) Note over API,UI: Old Date-based code paths keep working,<br/>only the conversion boundary changesvisualized by
// Incoming from an API, or from a library that still hands you a legacy Date
const legacyDate = new Date();
const instant = Temporal.Instant.fromEpochMilliseconds(legacyDate.getTime());
const zoned = instant.toZonedDateTimeISO('Asia/Kolkata');
const formatted = zoned.toLocaleString('en-IN', {
dateStyle: 'full',
timeStyle: 'short',
});
// If a third-party library still needs a legacy Date, convert back cleanly
const backToDate = new Date(zoned.epochMilliseconds);
toLocaleString on Temporal objects uses Intl.DateTimeFormat under the hood, so all your existing locale-formatting knowledge carries over directly, nothing new to learn there.
A sensible adoption order for an existing codebase: start with any code that already has visible date bugs (usually timezone displays or DST-adjacent scheduling logic), convert new features to Temporal from day one, and leave stable, bug-free legacy Date code alone until you touch it for other reasons. There is no requirement to do a big-bang rewrite, and the interop pattern above is exactly what makes that unnecessary.
Where things stand right now
| Environment | Status |
|---|---|
| Firefox | Native since Firefox 139 (May 2025) |
| Chrome / Edge | Native since Chrome 144 (January 2026) |
| Safari | Partial in Technology Preview, stable release pending |
| Node.js | Enabled by default since Node.js 26.0.0 (May 2026), LTS from October 2026 |
| TypeScript | Type definitions shipped in TypeScript 6.0 (March 2026), needs esnext target or lib |
| Older browsers / Node < 24 | Use @js-temporal/polyfill or temporal-polyfill |
For anything shipping to production today with a broad browser support matrix, the polyfill is still the safe default. For anything Node-only, or for a Chrome/Edge/Firefox-first internal tool, native support is realistically already there.
Practical takeaway
If you are working in an existing codebase, you do not need to rewrite anything today. Add the polyfill (or rely on native support if your target runtimes qualify), and use Temporal for new date and time logic going forward, especially anything involving timezones, durations, or scheduling. Convert at API boundaries using Temporal.Instant.fromEpochMilliseconds() on the way in and .epochMilliseconds on the way out, so legacy code and new code coexist without friction.
For interviews, this is a genuinely good signal to have ready: being able to explain why Date is mutable and Temporal is not, why a single Date instance cannot represent "this moment, viewed from two different timezones," and why ZonedDateTime arithmetic handles DST correctly while raw millisecond math does not, shows you understand the underlying time model rather than just the API surface. It is also a fresh enough topic (Stage 4 landed in March 2026) that most candidates will not have a sharp answer for it yet.
Conclusion
Date was broken by design decisions made in 1995 that nobody could safely change without breaking the entire web, so JavaScript lived with a mutable, timezone-blind, DST-unaware date object for three decades. Temporal fixes all of it: immutable types, explicit timezone handling, DST-correct arithmetic, nanosecond precision, and native calendar support, without removing Date or breaking a single line of existing code. It is finished, it is in the 2026 spec, and as of Node.js 26 and Chrome 144, it is no longer something you have to wait for.
Advertisement
Ready to practice?
Test your skills with our interactive UI challenges and build your portfolio.
Start Coding Challenge