JavaScript’s Temporal API (a complete replacement for the broken Date object) reached TC39 Stage 4 and is now shipping in production browsers. Chrome 144 shipped it in January 2026, Firefox 139 in May 2025. Safari support remains in progress. After nine years of proposals, debates, and false starts, JavaScript finally has a date and time system that works correctly.
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. Within two years, Java had deprecated most of that class’s own methods (in version 1.1, released 1997) because the design was already considered a mistake. JavaScript developers have been living with that mistake 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. This trips up experienced developers constantly.
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. Every defensive JavaScript codebase has date-copying boilerplate for exactly this reason.
Parsing behavior is implementation-defined. The spec left date string parsing intentionally vague. new Date("2026-03-27") can return different results across browsers and environments depending on how the string is interpreted.
Millisecond precision only. High-resolution timing use cases (benchmarking, animation, scientific computing) needed performance.now() as a workaround.
These aren’t edge cases. They’re the everyday reality of JavaScript date handling, which explains why date manipulation libraries collectively represent hundreds of millions of npm downloads per week.
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 Temporal proposal was formally introduced with a GitHub repository on March 12, 2017, championed initially by Philipp Dunkel and Maggie Johnson-Pint. (TC39 Proposal-Temporal GitHub Repository, created March 12, 2017) The core problem statement was clear from day one. The design work took years longer.
Stage 3 (the implementation phase) proved especially long. The proposal’s own documentation contained an unusual warning that held browser engineers back: “implementers of this proposal MUST NOT ship unflagged Temporal implementations” until the IETF had standardized timezone and calendar string serialization formats. That wasn’t just a polite note: it was a hard dependency on a separate standards body moving on its own schedule.
The nine-year gap between “everyone knows this is broken” and “it’s shipping in Chrome” reflects how standards actually evolve. It’s not one proposal stalling: it’s a cascade of dependencies across language design, spec precision, IETF standardization, and finally two independent browser implementations. Temporal hit Stage 4 in late 2024 and browsers began shipping immediately after.
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: ambiguous and mutation-prone const now = new Date(); const inTwoHours = new Date(now.getTime() + 2 * 60 * 60 * 1000);
// The Temporal way: explicit and immutable const 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 transition const 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 DST console.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-indexed const march2026Date = new Date(2026, 2, 27); // month 2 = March
// Temporal: months are 1-indexed const 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 nanoseconds
A Temporal.Duration exposes both a component field per unit (.nanoseconds is the 0-to-999 remainder after seconds, not the whole span) and a .total({ unit }) method that collapses the duration to a single number in the unit you ask for. Reaching for .nanoseconds to get elapsed time is the most common Temporal beginner bug: use .total() for arithmetic and reserve the component fields for formatting [Updated June 2026].
Immutability by Design
const date = Temporal.PlainDate.from(‘2026-03-27’); const nextWeek = date.add({ weeks: 1 });
// date is unchanged; nextWeek is a new object console.log(date.toString()); // 2026-03-27 console.log(nextWeek.toString()); // 2026-04-03
Calendar System Support
One underappreciated capability: Temporal natively supports non-Gregorian calendars. This matters for international applications targeting markets where Islamic, Hebrew, Japanese, or Chinese calendars have legal or cultural significance.
// Hebrew calendar date const hebrewDate = Temporal.PlainDate.from({ year: 5786, month: 7, day: 1, calendar: ‘hebrew’ });
// Convert to ISO for display alongside Gregorian dates const isoEquivalent = hebrewDate.withCalendar(‘iso8601’);
Previously this required specialized libraries with inconsistent APIs. Temporal builds it into the language.
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 (m.add(1, 'day') mutating m) have no equivalent: every operation returns a new value and you must reassign it. 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.
date-fns and Luxon are an easier exit than Moment, because both are immutable and both already push you toward explicit operations. Luxon was written by a Moment maintainer as a cleaner take on the same model, and its DateTime maps onto Temporal.ZonedDateTime with little friction. date-fns operates on plain Date objects, so migrating it is mostly a matter of swapping its standalone functions (addDays, differenceInDays, format) for the corresponding Temporal methods and dropping the Date carrier. Neither library escapes the underlying Date it wraps, which is the whole reason to move.
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 shift
The 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 throws a RangeError, 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. Covered above, and it bites everyone once. Useduration.total({ unit })for arithmetic; the per-unit properties on aTemporal.Durationare balanced remainders, not totals.- Round-tripping legacy
Date. Bridge withdate.toTemporalInstant()(installed by the polyfill and native once shipped) to go from aDateto aTemporal.Instant, andinstant.toZonedDateTimeISO(tz)to attach a zone. Avoid stringifying aDateand re-parsing it. - Time zones are required, not implied. Calling
.toZonedDateTime()without atimeZonethrows. There is no ambient default; that is deliberate. PlainDateTimehas no offset. Storing aPlainDateTimeand assuming it means UTC is the most common data-modeling error. 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 June 2026)
The rollout is substantial but not yet universal:
| Environment | Support | Version / Notes |
|---|---|---|
| Chrome | Native, unflagged | 144+ (January 13, 2026) |
| Edge | Native, unflagged | 144+ (tracks Chromium) |
| Firefox | Native, unflagged | 139+ (May 27, 2025, first to ship) |
| Safari | Tech Preview only, flagged | Disabled by default behind --useTemporal=1; no stable date [Updated June 2026] |
| Node.js | Native, unflagged | 26.0.0+ (May 5, 2026); flag-gated in 24 [Updated June 2026] |
| Deno | Native, unflagged | 2.7+ (February 2026) [Updated June 2026] |
| Bun | Not implemented | Tracking issue open; uses JavaScriptCore [Updated June 2026] |
| GraalJS | Planned | 25.1.0 |
Global coverage from caniuse stands at approximately 65% as of June 2026 [Updated June 2026]. (Can I Use: Temporal API) The roughly 35% gap is dominated by Safari and iOS, where no stable release ships Temporal yet. That coverage is enough for applications with Chrome/Firefox-dominant audiences, but not a safe baseline for production without polyfilling.
Using Temporal in Production Today
The safest path for immediate adoption:
npm install @js-temporal/polyfill
// Load only when native Temporal is unavailable if (typeof Temporal === ‘undefined’) { const { Temporal, Intl, toTemporalInstant } = await import(‘@js-temporal/polyfill’); globalThis.Temporal = Temporal; globalThis.Intl = Intl; Date.prototype.toTemporalInstant = toTemporalInstant; }
The @js-temporal/polyfill package, maintained by proposal champions Philip Chimento and Justin Grant, tracks the final spec. (Its README is explicit that the project is not an official TC39 product, just the champions’ reference implementation.) Code written against it today will work natively when Safari ships and the polyfill becomes dead weight you can drop without changing application code. One caveat worth knowing before you pin it: as of June 2026 the package sits at 0.5.1, last published in March 2025, so it predates a few late spec tweaks and the team has flagged a refresh to match the Stage 4 text [Updated June 2026].
A second option, temporal-polyfill by Adam Shaw of the FullCalendar project, offers a smaller bundle with a tree-shakeable API and is worth evaluating for bandwidth-sensitive applications. It is at 1.0.1 and now draws more weekly npm downloads than the champions’ package, so the “official one is the default” assumption no longer holds automatically [Updated June 2026].
What This Means for the Ecosystem
The broader implication is that JavaScript’s date library ecosystem is entering a consolidation phase. Moment.js already declared itself in maintenance mode in 2020, explicitly recommending users migrate away. (Moment.js Project Status) Luxon (created by a Moment.js author) and date-fns have positioned themselves as modern alternatives, but both remain wrappers around Date.
Temporal doesn’t just replace Moment: it eliminates the category of problem that made Moment necessary. Date arithmetic, timezone handling, and calendar operations are now a solved problem at the language level.
Library authors will need to decide: wrap Temporal for convenience APIs, or advise users to use Temporal directly. 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 [Updated June 2026]
Server-side adoption has its own timeline, and as of June 2026 it has overtaken the browsers. V8 (Node.js’s engine) tracks Chrome closely, but Node releases gate Temporal behind a separate API-stability review before exposing it as a default global. That review cleared in Node 26.0.0, released May 5, 2026 on the Current line, which ships a native Temporal global unflagged (V8 14.6) [Updated June 2026]. Older lines exposed an incomplete implementation behind --harmony-temporal; treat anything before Node 26 as flag-gated and partial. The LTS picture as of mid-2026: Node 24 is Active LTS, Node 22 is in maintenance, and Node 26 is scheduled to enter LTS in October 2026, so production teams pinned to LTS still polyfill for now.
The other runtimes split. Deno shipped Temporal as a stable, default-on global in Deno 2.7 (February 2026), dropping the earlier --unstable-temporal requirement [Updated June 2026]. Bun has not shipped it at all: it runs on JavaScriptCore, the same engine as Safari, and its Temporal tracking issue is still open [Updated June 2026]. (For more on Bun’s internals, see Bun’s rewrite from Zig to Rust.) For background on how this field is moving, see Deno 2.8 and the reshuffling JavaScript runtime hierarchy.
For application teams, the practical implication is that server-side adoption can run ahead of browser adoption when you control the runtime. A backend on Node 26 or Deno 2.7 can write native Temporal today; a backend pinned to Node 24 LTS keeps the polyfill and swaps it out at the next LTS bump without rewriting any calendar arithmetic, because the polyfill matches the shipped spec. This inverts the usual web-standards story, where the front-end leads and the server lags.
The Lesson in How Standards Evolve
Temporal’s nine-year arc is worth understanding as a model for how web standards actually advance. The JavaScript community recognized the Date problem in 1997. Popular solutions (moment.js launched in 2011) emerged and captured millions of users. TC39 began formal work in 2017. Browser implementations followed years later. Safari will presumably ship in 2026 or 2027.
Each delay had a specific cause: design debates about API shape, the external dependency on IETF’s RFC 9557, implementation complexity in browser engines. The nine years weren’t bureaucratic inertia: they were the time required to design a correct solution, not just a better one.
The result is an API with no obvious design debt. Where Date borrowed mistakes from Java, Temporal was designed by people who spent years thinking about 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.
Frequently Asked Questions
Q: Can I use Temporal in production today? A: Yes, with a polyfill. Install @js-temporal/polyfill from the proposal champions or temporal-polyfill from FullCalendar for a smaller bundle. Both implement the final spec, so code written now will work natively once Safari ships with no changes required.
Q: Will Temporal replace Moment.js and date-fns? A: For new code, yes. Moment.js is already in maintenance-only mode. Once Temporal achieves full browser coverage (likely when Safari ships), there’s no remaining justification for adding a date library for standard use cases. Temporal provides timezone arithmetic, immutability, calendar support, and nanosecond precision natively.
Q: What TC39 stage is Temporal at? A: Stage 4, the final stage, reached on March 11, 2026, which means the proposal is complete and merged into the ECMAScript specification (it ships as part of ES2026) [Updated June 2026]. Chrome 144 (January 2026) and Firefox 139 (May 2025) had already shipped native implementations ahead of the formal vote.
Q: Does Temporal work in Node.js? A: Yes, natively and unflagged in Node 26.0.0 and later, which shipped in May 2026 [Updated June 2026]. Earlier Node lines only had a partial implementation behind --harmony-temporal, so if you are pinned to Node 24 LTS or older, keep using @js-temporal/polyfill until you upgrade. Deno has it default-on since 2.7; Bun does not ship it yet.
Q: What’s the difference between Temporal.Instant and Temporal.ZonedDateTime? A: Instant represents a single point in universal time, like a Unix timestamp, with no timezone context. ZonedDateTime is that same instant interpreted in a specific timezone, giving you wall-clock time, DST-aware arithmetic, and local date/time components. Use Instant for timestamps and ordering; use ZonedDateTime for scheduling and display.