JavaScript’s Temporal API, a complete replacement for the broken Date object, reached Stage 4 at TC39’s March 2026 plenary (Igalia) and is shipping in production browsers: Chrome 144 in January 2026 and Firefox 139 in May 2025, per the proposal’s implementation status. Safari is the major holdout; no stable release includes it as of October 2026 (Can I Use). Nine years of design work and standardization later, JavaScript has a date and time system that treats timezones and calendars as first-class concerns.
Why JavaScript’s Date Object Was Always Broken
The Date object was created in ten days in 1995. Brendan Eich, under orders to “make it like Java,” copied the API directly from java.util.Date. Nearly all of that class’s methods were deprecated and replaced in Java 1.1 in 1997, yet JavaScript developers have lived with the copy ever since.
The problems aren’t cosmetic. They cause real bugs:
Month numbering starts at zero. new Date(2026, 0, 1) is January 1st. new Date(2026, 12, 1) silently overflows into the next year. The zero-based month is baked into the original API and has never been correctable without breaking the web.
No timezone support beyond local and UTC. If you need to work with time in New York while your server runs in UTC and your user is in Tokyo, Date forces you to do the math yourself, with no DST awareness.
DST arithmetic is unpredictable. Adding 24 hours to a date near a daylight saving transition doesn’t always land where you expect. The Date API has no concept of “the next calendar day at the same wall-clock time.”
Mutation causes subtle bugs. Date objects are mutable. Pass one to a function and that function can silently modify it, so any caller that needs the original value has to copy it first. The champions’ own analysis put mutability among the problems that could not be fixed in Date at all (Fixing JavaScript Date).
Parsing behavior is unreliable. The original champions’ own problem list called the parser “so unreliable it is unusable.” String interpretation varies across browsers and environments.
Millisecond precision only. High-resolution timing use cases needed performance.now() as a workaround; Temporal adds nanosecond precision (Igalia).
These aren’t edge cases. They’re the everyday reality of JavaScript date handling, and the reason an entire library category grew up around one built-in object.
How the TC39 Process Works (and Why It Took Nine Years)
Understanding the timeline requires understanding TC39, the committee that governs ECMAScript standardization. Every language feature progresses through five numbered stages:
| Stage | Name | Meaning |
|---|---|---|
| 0 | Strawperson | Idea submitted for discussion |
| 1 | Proposal | Problem statement, initial solution, champions assigned |
| 2 | Draft | Precise syntax and semantics described |
| 3 | Candidate | Spec complete, awaiting implementation feedback |
| 4 | Finished | Two shipping implementations, merged into spec |
The champion roster eventually ran to nine people (TC39 proposal repository), growing out of the effort Maggie Johnson-Pint, Matt Johnson, and Brian Terlson started after cataloguing the problems above (Fixing JavaScript Date). Igalia’s Philip Chimento and Ujjwal Sharma co-championed the final stretch, and Bloomberg funded the work and contributed engineering directly (Igalia).
Stage 3, the implementation phase, proved especially long, and one reason sat outside TC39 entirely. Temporal needs a standard way to serialize timestamps that carry timezone and calendar information, but the conventions in wide use, like appending IANA timezone names to timestamps, had never been on a formal standards track (Igalia). That sent part of the work to the IETF, a separate standards body on its own schedule.
The nine-year gap between “everyone knows this is broken” and “it ships in Chrome” reflects how standards actually evolve: dependencies cascading across language design, spec precision, IETF standardization, and finally independent implementations. Temporal reached Stage 4 at TC39’s 113th plenary in March 2026 (Igalia), with Chrome and Firefox already shipping ahead of the formal vote. Igalia notes the finished API carries around 4,500 test262 tests; Date, for comparison, has 594.
What Temporal Actually Looks Like
Temporal is not a class: it’s a namespace, like Math or Intl. You never write new Temporal(). Instead, you use specific classes within the namespace, each designed for a particular purpose.
// The old way: arithmetic and mutation by handconst now = new Date();const inTwoHours = new Date(now);inTwoHours.setHours(inTwoHours.getHours() + 2);
// The Temporal way: explicit and immutableconst now = Temporal.Now.instant();const inTwoHours = now.add({ hours: 2 });The key classes map directly to real concepts:
| Class | Use Case | Example |
|---|---|---|
Temporal.Instant | A precise moment in time (UTC) | Timestamps, logs, event ordering |
Temporal.ZonedDateTime | A moment with timezone context | Scheduling meetings, alarms |
Temporal.PlainDate | A calendar date with no time | Birthdays, holidays, deadlines |
Temporal.PlainTime | A wall-clock time with no date | Business hours, recurring daily times |
Temporal.PlainDateTime | Date + time, no timezone | Local form inputs, database storage |
Temporal.Duration | A span of time | “3 days 2 hours from now” |
Temporal.PlainYearMonth | Month + year only | Monthly billing periods |
Temporal.PlainMonthDay | Month + day only | Anniversaries, recurring yearly dates |
DST-Safe Arithmetic
The most practically important improvement is timezone-aware arithmetic:
// Schedule "9am New York" the day after DST transitionconst meetingDay = Temporal.PlainDate.from('2026-03-08');const nextDay = meetingDay.add({ days: 1 });
const meeting = nextDay.toZonedDateTime({ timeZone: 'America/New_York', plainTime: Temporal.PlainTime.from('09:00')});
// Always 9:00 AM New York time, regardless of DSTconsole.log(meeting.toString());// 2026-03-09T09:00:00-04:00[America/New_York]With Date, this calculation would silently produce 10:00 AM or 8:00 AM depending on DST state unless you wrote careful manual correction code.
Month Numbers That Make Sense
// Date: months are 0-indexedconst march2026Date = new Date(2026, 2, 27); // month 2 = March
// Temporal: months are 1-indexedconst march2026 = Temporal.PlainDate.from({ year: 2026, month: 3, day: 27 });Nanosecond Precision
const start = Temporal.Now.instant();// ... some operation ...const end = Temporal.Now.instant();const elapsed = end.since(start);
// Read the TOTAL span, not the component field:console.log(elapsed.total({ unit: 'nanoseconds' })); // precise to nanosecondsA Temporal.Duration exposes a component field per unit and a .total({ unit }) method that, per MDN, returns “a floating-point number representing the total duration in the given unit.” Those are different quantities: the component fields describe the duration’s parts, while .total() answers how long the whole span was in a single unit. Use .total() for elapsed-time arithmetic and reserve the component fields for formatting.
Immutability by Design
const date = Temporal.PlainDate.from('2026-03-27');const nextWeek = date.add({ weeks: 1 });
// date is unchanged; nextWeek is a new objectconsole.log(date.toString()); // 2026-03-27console.log(nextWeek.toString()); // 2026-04-03Immutability is a stated principle of the proposal: all Temporal objects are immutable (TC39 proposal repository).
Calendar System Support
One underappreciated capability: Temporal natively supports non-Gregorian calendars, including Japanese, Hebrew, Persian, Chinese, and Islamic (Igalia). This matters for international applications where those calendars have legal or cultural significance.
// Interpret an ISO date in the Hebrew calendarconst hebrewDate = Temporal.PlainDate.from('2026-03-27').withCalendar('hebrew');
// Project back to ISO for display alongside Gregorian datesconst isoEquivalent = hebrewDate.withCalendar('iso8601');Previously this required specialized libraries with inconsistent APIs. Temporal builds it into the language, and a companion ECMA-402 proposal specifies the era and monthCode details each calendar needs for consistent cross-implementation behavior (Igalia).
Migrating Off Date, Moment, date-fns, and Luxon
Most teams will not rewrite working code on day one. The realistic path is to stop adding new Date and library calls, then convert hot paths (timezone math, recurring schedules, calendar arithmetic) where the old approach was already fragile. The mapping is mechanical for the common operations:
| Task | Date / Moment | Temporal |
|---|---|---|
| Current instant | Date.now() / moment() | Temporal.Now.instant() |
| Now in a zone | moment.tz('Asia/Tokyo') | Temporal.Now.zonedDateTimeISO('Asia/Tokyo') |
| Parse a calendar date | new Date('2026-06-20') | Temporal.PlainDate.from('2026-06-20') |
| Add 1 month | moment().add(1, 'month') | date.add({ months: 1 }) |
| Difference in days | b.diff(a, 'days') | a.until(b).total({ unit: 'days' }) |
| Format ISO | date.toISOString() | zdt.toString() / instant.toString() |
| Compare ordering | a.getTime() < b.getTime() | Temporal.Instant.compare(a, b) < 0 |
Two structural differences trip up the mechanical translation. First, Temporal objects are immutable, so the in-place mutation idioms that Moment encourages have no equivalent: every operation returns a new value and you must reassign it. Moment’s documentation is explicit that its setter methods “mutate the original moment when used as setters” (Moment.js docs). Second, Temporal forces you to be explicit about whether a value carries a time zone. Moment blurred wall-clock time and absolute time into one object; Temporal splits them across PlainDateTime (no zone) and ZonedDateTime (zoned), and it will not silently guess. That explicitness is the point, but it means a one-to-one port of Moment code often surfaces a latent bug about which kind of time the original code actually meant.
Moment also sits directly on top of Date: its overloaded getters and setters “map to the corresponding function on the native Date object”, its docs state (Moment.js docs). That is the layer Temporal eliminates. date-fns and Luxon have different APIs again, so check their documentation for what their objects assume about mutability and time zones before porting; the Moment mapping above will not necessarily carry over.
A worked end-to-end example: compute the next three monthly billing dates for a customer in Berlin, anchored to the 31st where the month allows it and clamped otherwise, with each boundary expressed as an absolute instant for storage.
const anchor = Temporal.PlainDate.from({ year: 2026, month: 1, day: 31 });
const billingDates = [1, 2, 3].map((n) => anchor .add({ months: n }, { overflow: 'constrain' }) // Feb 31 clamps to Feb 28 .toZonedDateTime({ timeZone: 'Europe/Berlin', plainTime: Temporal.PlainTime.from('00:00'), }) .toInstant() // absolute UTC for storage .toString());
console.log(billingDates);// Feb, Mar, Apr boundaries, each correct across the March 29 DST shiftThe overflow: 'constrain' option is the kind of decision Date made for you silently and usually wrong: it clamps an impossible day (February 31) to the last valid day of the month instead of rolling into March. Pass overflow: 'reject' instead and Temporal refuses the impossible date rather than clamping it, which is often what a billing system actually wants. The old Date constructor had exactly one behavior here (overflow into the next month) and no way to ask for the other.
A few gotchas worth knowing before a migration:
.total()versus component fields. Useduration.total({ unit })for arithmetic; the per-unit properties on aTemporal.Durationare balanced components of the span, not totals.- Round-tripping legacy
Date. Convert explicitly at the boundary rather than stringifying aDateand re-parsing the result; re-parsing hands the string back to a parser the champions’ problem list called “so unreliable it is unusable”. The proposal repository links the reference documentation and a cookbook covering interop patterns. - Time zones are explicit, not implied. Conversions that need a zone take a
timeZoneargument; the API is designed not to guess an ambient default, and that is deliberate. PlainDateTimehas no offset. Storing aPlainDateTimeand later assuming it meant UTC is a data-modeling mistake the type itself cannot flag. If a value is meant to be absolute, store anInstant; if it is meant to be wall-clock in a place, store aZonedDateTime.
Browser and Runtime Support Status (As of October 2026)
The rollout is substantial but not universal. The version data below comes from the proposal’s implementation status and caniuse:
| Environment | Support | Version / Notes |
|---|---|---|
| Chrome | Native, unflagged | 144+ (January 13, 2026) |
| Edge | Native, unflagged | 144+ |
| Firefox | Native, unflagged | 139+ (May 27, 2025; first stable browser) |
| Safari | Preview only | Not in stable releases through 27.2; Technology Preview supported |
| Node.js | Native, unflagged | 26.0.0+ (May 5, 2026) |
| Deno | Native, unflagged | 2.7+ (February 2026) |
| GraalJS | Planned | GraalVM 25.1.0 |
The caniuse matrix shows the shape of the gap: current Chromium and Firefox releases are supported, every listed stable Safari and iOS Safari release is not, and several secondary browsers (Samsung Internet, Opera Mini, KaiOS among them) remain without support (Can I Use). That is enough for applications with Chrome- and Firefox-dominant audiences, but not a safe baseline for production without polyfilling. Tooling is further along than the browser matrix suggests: TypeScript 6.0 beta already includes Temporal (Igalia).
Using Temporal in Production Today
The safest path for immediate adoption:
npm install @js-temporal/polyfill// Load only when native Temporal is unavailableif (typeof Temporal === 'undefined') { const { Temporal } = await import('@js-temporal/polyfill'); globalThis.Temporal = Temporal;}Know what you are pinning. Igalia describes @js-temporal/polyfill as “the reference polyfill developed by the champions” (Igalia), but the proposal’s own polyfill table labels it “Alpha release available” (TC39 proposal repository). The same table lists fullcalendar’s temporal-polyfill as “Stable release available” and temporal-polyfill-lite as “Release Candidate available”. If you want the implementation closest to the spec process, the champions’ package is it, alpha label and all; if your risk posture wants a package its own project calls stable, the fullcalendar option is the listed alternative. Either way, code written against the polyfill’s API shape carries forward to native support, though audit anything that depends on the newest spec details.
What This Means for the Ecosystem
Moment.js describes itself as “a legacy project in maintenance mode” (Moment.js Project Status), and its own documentation maps its get/set methods onto the native Date object, with setters that mutate in place. That is the surface Temporal retires.
Temporal moves the category of problem that made Moment necessary into the language. Date arithmetic, timezone handling, and calendar operations no longer require a dependency to get right. Library authors now face a simpler choice: wrap Temporal for convenience APIs, or point users at the native API. For most calendar and scheduling use cases, the native API is already expressive enough that wrappers add overhead without value.
Node.js and Server-Side State
Server-side adoption has overtaken the browsers. Node 26.0.0, released May 5, 2026 on the Current line, ships the Temporal API enabled by default on V8 14.6; the proposal’s implementation status lists Node 26 as the shipping version (TC39 proposal repository). Node 26 becomes LTS in October 2026 per the release notes, so the LTS line is at a cutover: once 26 is your LTS, the native Temporal global is simply there. On earlier lines, verify support before dropping the polyfill.
Deno shipped Temporal as a stable, default-on global in Deno 2.7 (February 2026), dropping the earlier --unstable-temporal requirement; the release notes credit the V8 14.5 upgrade that followed Chrome 144’s January 2026 ship.
The practical implication: when you control the runtime, server-side adoption can run ahead of the browser picture. A backend on Node 26 or Deno 2.7 writes native Temporal today, while a browser-facing app still carries the polyfill until Safari lands. That inverts the usual web-standards story, where the front end leads and the server follows.
The Lesson in How Standards Evolve
The flaw dates to 1995, and Java deprecated the borrowed API within two years. Fixing it properly consumed the better part of a decade, split across API design debates, an IETF dependency (RFC 9557), and implementation work in multiple engines. Igalia’s summary of the arc is blunt: “It took nine years and a lot of people.”
The result is an API whose design lines up with the problems that motivated it. Where Date inherited someone else’s mistake, Temporal was designed by people who spent years deciding what JavaScript date handling should look like if you started from scratch. The immutability, the specialized classes, the DST safety, the calendar support: these aren’t features bolted on. They’re the foundation, and they are what nine years bought: not a bigger Date, but a date and time model where the hard cases have specified answers.

Join the discussion
Share a useful perspective or ask a question about this article.