
The JavaScript Date object shipped in 1995, copied from a deprecated Java API, and has been punishing developers ever since. Zero-indexed months. Silent mutations. Timezone handling that changes behavior based on server locale. The State of JS survey named date handling the #2 pain point in the language for years straight. This year, the wait ended. TC39 Temporal reached Stage 4 in March 2026. Node.js 26 ships it unflagged by default. Chrome, Firefox, and Safari have all rolled it out. The Date object is now optional — here’s how to stop using it.
Why Date Has Always Been a Trap
Before switching to Temporal, it’s worth naming the failure modes precisely — because a few of them are subtle enough to bite you even when you think you’re being careful.
Months are 0-indexed. new Date(2026, 0, 1) is January 1st. The year 2026 and day 1 are what you’d expect, but month 0 means January. Every JavaScript developer has shipped a bug from this at least once.
Date objects are mutable. Pass a Date to any function, and that function can quietly modify it. Assign one Date to another with const b = a, and you don’t have a copy — you have two references to the same mutable object. This is the kind of bug that lives in production for months before someone notices.
Timezone support is a fiction. Date only understands UTC and the local timezone of whatever JavaScript runtime it’s running in. There’s no way to work with “America/New_York” as a named timezone without a third-party library. And new Date("2026-06-25 15:15:00") produces different results depending on whether you’re in Node.js, Chrome, or Firefox. That’s not ambiguity — that’s a correctness bug.
Overflow is silent. Try creating February 31st. Date won’t throw — it’ll quietly return March 3rd. No warning, no error.
The Four Temporal Types You Actually Need
Temporal’s type system looks intimidating at first. In practice, most code only needs four types — each one designed to eliminate a specific category of bug.
Temporal.PlainDate is for calendar dates with no time component: birthdays, billing cycle start dates, deadlines. When the hour doesn’t matter and the timezone shouldn’t affect the answer, use PlainDate.
Temporal.ZonedDateTime is for scheduling. It carries a full IANA timezone identifier — America/New_York, not just -04:00 — so it knows when DST starts and ends. Add one day to a ZonedDateTime set to 9 AM on a DST boundary and you still get 9 AM the next day. Not 8 AM, not 10 AM.
Temporal.Instant is for UTC timestamps: database writes, API payloads, logging, event sourcing. Think of it as the replacement for new Date().toISOString(), but unambiguous about what timezone it represents.
Temporal.Duration is for time spans: billing intervals, session lengths, rate-limit windows. No more Math.floor(diffMs / (1000 * 60 * 60 * 24)) with a comment explaining what that magic number means.
Three Migration Patterns
These cover the majority of real-world Date usage. Apply them incrementally — you don’t need to migrate everything at once.
Pattern 1: Current Timestamp
// Before
const now = new Date().toISOString();
// After
const now = Temporal.Now.instant().toString();
// Output: "2026-05-24T14:30:00.000000000Z"
Temporal.Now.instant() is always UTC, nanosecond-precise, and unambiguous. No more wondering whether the server’s local timezone contaminated your timestamp.
Pattern 2: Calendar Date (the month-index fix)
// Before — month 0 = January (confusing)
const start = new Date(2026, 0, 15);
// After — month 1 = January (obvious)
const start = Temporal.PlainDate.from({ year: 2026, month: 1, day: 15 });
Every place you use new Date(year, month - 1, day) or subtract 1 from a month number, switch to PlainDate.from(). This is the highest-impact single migration in most codebases.
Pattern 3: Date Arithmetic Across DST
// Before — wrong on DST boundaries
const tomorrow = new Date(meeting.getTime() + 86400000);
// After — always correct
const tomorrow = meeting.add({ days: 1 }); // ZonedDateTime handles DST automatically
Adding 86,400,000 milliseconds is not the same as “tomorrow” when clocks change. ZonedDateTime.add({ days: 1 }) knows the difference. This is where Temporal earns its keep for any scheduling application.
What to Do With Moment.js
The Moment.js team put the library in maintenance mode in 2020 and explicitly recommends against it in new projects. With Temporal shipping natively across Node.js and major browsers, there’s no reason to reach for Moment, date-fns, or day.js in new code.
For existing codebases with Moment, don’t do a big-bang rewrite. Use Temporal for new functionality and migrate Moment-dependent code file by file as you touch it. The two coexist without conflict during the transition.
When You Can Use Temporal Right Now
- Node.js 26 (current release): Temporal enabled by default. No flags, no install, works today.
- Modern browsers: Chrome 129+, Firefox 139+, and Safari 18.4+ ship Temporal natively.
- Node.js 24 LTS / older browsers: Install @js-temporal/polyfill — identical API, automatically deactivates as native support lands.
- TypeScript: Full type definitions ship with TypeScript 6.0 — no
@typespackage needed.
If your project targets Node.js 26 or modern browsers exclusively, setup overhead is zero. For broader targets, one npm install is all it takes.
The End of a 30-Year Compromise
The TC39 Temporal proposal took nine years to move from initial idea to Stage 4. That’s a long time to wait for a fix that should have been built in correctly in 1995. But it’s here now, and Node.js 26 ships it by default. The official Temporal documentation covers every edge case the old API left ambiguous.
There’s no longer a good argument for writing new Date(year, month - 1, day). The compromise is over.













