If you've ever written new Date() in JavaScript and felt a little piece of your soul die, you're not alone. JavaScript's Date object has been the butt of developer jokes since 1995 — mutable by default, zero-indexed months, no timezone support worth mentioning, and parsing behavior that varies across browsers. We've been duct-taping over it with libraries like Moment.js, date-fns, and Luxon for years.
Well, the duct tape era is officially over. The Temporal API reached TC39 Stage 4 on March 11, 2026, making it part of the ES2026 specification. It's already shipping natively in Chrome 144+, Firefox 139+, and Edge 144+. This isn't a proposal anymore — it's the standard. Let's dig into what it gives you, how to use it, and why you should start migrating today.
Why the Date Object Was Always Broken
Before we celebrate the new thing, let's pour one out for Date and remember exactly why it needed replacing. The problems aren't subtle — they're fundamental design flaws that have caused countless production bugs.
Mutability is the root of all evil. When you call date.setMonth(5), it mutates the original object in place. If you passed that date to three different functions, congratulations — they're all sharing the same mutable reference. Bugs like this are notoriously hard to track down.
Months start at zero. January is 0, December is 11. This has tripped up every single JavaScript developer at least once. It's the kind of API decision that makes you wonder if someone was having a bad day in 1995.
Timezone support is basically nonexistent. The Date object only understands UTC and the user's local timezone. Need to display a time in Tokyo while the user is in New York? Good luck — you're reaching for a library.
Parsing is a minefield. Try new Date("2026-03-31") in different browsers and you might get different results. The spec is intentionally vague about how date strings should be parsed, which means cross-browser inconsistencies are a feature, not a bug.
Meet the Temporal API: The Right Tool for Every Job
The Temporal API doesn't try to fix Date — it replaces it entirely with a set of purpose-built types. Instead of one object trying to do everything (and doing it all poorly), Temporal gives you specialized types for different use cases. Think of it like Temporal is a namespace, similar to Math or Intl.
Here are the types you'll use most often:
Temporal.PlainDate— A calendar date with no time or timezone. Perfect for birthdays, due dates, and holidays.Temporal.PlainTime— A wall-clock time with no date or timezone. Think "meeting at 2:30 PM."Temporal.PlainDateTime— Date + time, still no timezone. Great for local events like "March 31 at 9:00 AM."Temporal.ZonedDateTime— The full package: date, time, timezone, and calendar. This is what you use for exact moments in specific locations.Temporal.Instant— A precise point on the universal timeline, measured in nanoseconds since the Unix epoch. Use this for timestamps and logging.Temporal.Duration— Represents a length of time (e.g., 2 hours and 30 minutes). Essential for date math.
The key insight: by separating these concerns into distinct types, Temporal makes it impossible to accidentally mix up a "wall-clock time" with an "exact instant." The type system catches your mistakes before they become production bugs.
Getting Started: Creating Dates the Right Way
Let's see Temporal in action. The API is refreshingly intuitive compared to Date.
Creating a Plain Date
// From a string
const birthday = Temporal.PlainDate.from('1990-05-15');
console.log(birthday.year); // 1990
console.log(birthday.month); // 5 (not 4!)
console.log(birthday.dayOfWeek); // 3 (Tuesday)
// From an object
const launch = Temporal.PlainDate.from({ year: 2026, month: 3, day: 31 });
// Using the constructor
const deadline = new Temporal.PlainDate(2026, 12, 25);Notice that months are 1-indexed. January is 1, December is 12. The way it always should have been.
Working with Time and DateTime
// Just a time
const meetingTime = Temporal.PlainTime.from('14:30:00');
console.log(meetingTime.hour); // 14
console.log(meetingTime.minute); // 30
// Date + Time
const event = Temporal.PlainDateTime.from('2026-03-31T09:00:00');
console.log(event.toPlainDate()); // 2026-03-31
console.log(event.toPlainTime()); // 09:00:00Each type knows how to extract its component parts. A PlainDateTime can give you its date or time half. Clean, predictable, no surprises.
Immutability: No More Accidental Mutations
This is arguably the biggest win. Every Temporal object is immutable. Operations like add(), subtract(), and with() always return new objects — they never modify the original.
const date = Temporal.PlainDate.from('2026-03-31');
const nextWeek = date.add({ days: 7 });
console.log(date.toString()); // '2026-03-31' (unchanged!)
console.log(nextWeek.toString()); // '2026-04-07'
// Compare with the old way:
const oldDate = new Date('2026-03-31');
oldDate.setDate(oldDate.getDate() + 7); // mutates in place
// original value is gone foreverThis alone eliminates an entire category of bugs. You can pass Temporal objects around freely without worrying about some distant function mutating your data.
Timezone Handling That Actually Works
This is where Temporal really flexes. ZonedDateTime gives you first-class IANA timezone support — no more offset juggling or library-dependent workarounds.
// Get the current time in specific timezones
const nowInTokyo = Temporal.Now.zonedDateTimeISO('Asia/Tokyo');
const nowInNewYork = Temporal.Now.zonedDateTimeISO('America/New_York');
const nowInLondon = Temporal.Now.zonedDateTimeISO('Europe/London');
console.log(nowInTokyo.toString());
// '2026-03-31T22:00:00+09:00[Asia/Tokyo]'
// Convert between timezones
const meeting = Temporal.ZonedDateTime.from({
year: 2026, month: 4, day: 1,
hour: 10, minute: 0,
timeZone: 'America/Los_Angeles'
});
const meetingInLondon = meeting.withTimeZone('Europe/London');
console.log(meetingInLondon.hour); // 18 (6 PM)Temporal also handles DST transitions correctly. The hoursInDay property tells you if a day has 23 or 25 hours due to a clock change, and getTimeZoneTransition('next') lets you find exactly when the next DST shift happens. This used to require heavyweight libraries — now it's built in.
Duration and Date Math Made Simple
Calculating the difference between two dates was always awkward with Date. You'd subtract timestamps, divide by magic numbers (86400000 anyone?), and pray the math worked across month boundaries. Temporal's Duration type handles all of this cleanly.
const start = Temporal.PlainDateTime.from('2026-03-01T09:00:00');
const end = Temporal.PlainDateTime.from('2026-03-31T17:30:00');
const duration = start.until(end);
console.log(duration.toString()); // 'P30DT8H30M'
console.log(duration.total({ unit: 'hours' })); // 728.5
console.log(duration.total({ unit: 'days' })); // 30.354...
// Create durations directly
const sprint = Temporal.Duration.from({ weeks: 2 });
const sprintEnd = Temporal.PlainDate.from('2026-03-31').add(sprint);
console.log(sprintEnd.toString()); // '2026-04-14'The until() and since() methods work on all Temporal types. And Duration.total() lets you convert any duration into a single unit — no more manual math.
Temporal.Now: Your New Best Friend
Getting the current date and time is cleaner than ever. Temporal.Now gives you exactly the type you need:
// Current instant (for timestamps)
const timestamp = Temporal.Now.instant();
// Current date in your timezone
const today = Temporal.Now.plainDateISO();
// Current date-time in a specific timezone
const nowHere = Temporal.Now.zonedDateTimeISO();
const nowThere = Temporal.Now.zonedDateTimeISO('Europe/Berlin');No more new Date().toISOString().slice(0, 10) hacks to get today's date as a string. Just Temporal.Now.plainDateISO().toString() — done.
Comparing Dates: No More getTime() Tricks
Temporal provides a proper compare() static method on every type, plus equals() for value equality:
const d1 = Temporal.PlainDate.from('2026-03-31');
const d2 = Temporal.PlainDate.from('2026-04-15');
// Comparison (returns -1, 0, or 1)
Temporal.PlainDate.compare(d1, d2); // -1 (d1 is earlier)
// Sorting
const dates = [d2, d1];
dates.sort(Temporal.PlainDate.compare); // [d1, d2]
// Value equality
const d3 = Temporal.PlainDate.from('2026-03-31');
console.log(d1.equals(d3)); // trueUse .equals() for value comparison — never ===. Two Temporal objects with the same values are separate instances, so strict equality won't work. This is the same pattern as comparing strings in Java — the API is consistent and predictable.
Browser Support and the Polyfill Story
As of March 2026, native Temporal support is rolling out across major browsers:
Chrome 144+ — Full support
Edge 144+ — Full support (Chromium-based)
Firefox 139+ — Full support
Safari — In progress, behind a flag
Node.js 24+ — Full support
Deno & Bun — Full support
For older environments, there are solid polyfills available. The temporal-polyfill package is lightweight and spec-compliant. Install it and you're good to go:
npm install temporal-polyfillimport { Temporal } from 'temporal-polyfill';
// Use the same API — it's spec-compliant
const today = Temporal.Now.plainDateISO();Migrating from Date (and from Moment/date-fns)
You don't have to rip out every Date usage overnight. Here's a practical migration path:
Start with new code. Use Temporal for all new date logic. Don't introduce more Date usage.
Convert at the boundaries. When you receive a Date from an API or library, convert it to Temporal immediately. When you need to pass a Date to something that requires it, convert at the last moment.
Replace library imports gradually. If you're using date-fns for timezone support, Temporal handles that natively now. Start replacing those imports one module at a time.
Use Instant for interop.
Temporal.Instant.fromEpochMilliseconds(date.getTime())bridges the old world to the new.
// Converting from legacy Date to Temporal
const legacyDate = new Date();
const instant = Temporal.Instant.fromEpochMilliseconds(legacyDate.getTime());
const zoned = instant.toZonedDateTimeISO('America/New_York');
// Converting back when needed
const backToDate = new Date(zoned.epochMilliseconds);Gotchas and Tips
A few things to watch out for as you start using Temporal:
Don't mix types. You can't add a PlainDate to a ZonedDateTime directly. Temporal enforces type safety — convert explicitly.
Use .equals() for comparison, not ===. Two objects with identical values are still different references.
Prefer .from() over the constructor. The static .from() method is more flexible and handles strings, objects, and other Temporal types.
Watch your calendar. Temporal supports non-Gregorian calendars (Hebrew, Islamic, Japanese, etc.). The default is ISO 8601, which is what you want 99% of the time.
Nanosecond precision. Unlike Date's millisecond precision, Temporal measures time in nanoseconds. This matters for high-frequency logging and scientific applications.
Wrapping Up
The Temporal API is the single biggest quality-of-life improvement for JavaScript developers in years. It fixes every major pain point of the Date object — mutability, timezone chaos, zero-indexed months, unreliable parsing — and replaces it with a thoughtfully designed, type-safe, immutable API that just works.
Here's your TL;DR:
Temporal is ES2026 standard — Stage 4, shipping in Chrome, Firefox, Edge, and Node.js.
Six core types for different use cases: PlainDate, PlainTime, PlainDateTime, ZonedDateTime, Instant, Duration.
Everything is immutable — no more accidental mutations.
Native timezone support with IANA timezone IDs — no more library dependencies.
Start using it today with the temporal-polyfill package for older environments.
Migrate incrementally — new code uses Temporal, convert legacy Date at the boundaries.
After 30 years of new Date() pain, JavaScript finally has a date/time API we can be proud of. Go give it a spin — your future self will thank you.