Working with Dates in JavaScript Using the Temporal API
JavaScript recently gained a full replacement for the Date object: Temporal. It reached TC39 Stage 4 in March 2026 and is now part of the ES2026 specification. It's expected to be Baseline Newly Available in 2026!
In this article, we'll look at why Temporal exists, how its main types work, and how you can start using it today.
If you just want a side-by-side reference of Date vs Temporal patterns, I've put together a Temporal cheatsheet you can keep open in another tab — this article is the longer-form explanation.
Why a new date API?
Before we get into Temporal, it's worth understanding why the existing Date object wasn't just patched.
Date has been in JavaScript since 1995. It was modeled on an old Java API (java.util.Date) which Java itself deprecated two years later. We've been living with it ever since.
Three problems come up over and over:
Dateis mutable. Assigning a date to another variable shares a reference, so callingsetDate()on one mutates the other. This is the source of countless silent bugs.- Months are zero-indexed, but days are one-indexed.
new Date(2026, 2, 15)is March 15th, not February 15th. Nobody has ever found this intuitive. - There's no real time zone support. You get UTC or whatever the user's OS is set to but nothing else. No
Europe/Amsterdam, noAmerica/New_York.
You've probably hit at least one of these. They're the reason libraries like moment, date-fns, and dayjs exist, and the reason their combined weekly downloads are well over 100 million.
The downside is real though: every site that pulls in one of these libraries ships extra kilobytes to every visitor, often just to format a date. The libraries that grew up around Date also helped shape what Temporal needed to be. The proposal champions gathered years of ecosystem pain points to figure out which problems actually mattered.
Temporal is the language's own answer to these problems. It's a new global namespace (like Math or Intl), it's immutable by default, and it handles time zones as a first-class concept, while also providing types for modeling dates and times without time zones and durations.
The shape of the API
Temporal is a namespace, not a class. You cannot call new Temporal() as it is not a constructor. Instead, you use one of the types inside it, picking the one that matches what you're actually modeling.
There are seven types, and choosing the right one is most of the learning curve:
Temporal.Instant: an specific point in time, in UTC, with no calendar or time zone attached.Temporal.ZonedDateTime: an instant paired with a specific time zone (e.g.Europe/Paris).Temporal.PlainDateTime: a date and time with no time zone.Temporal.PlainDate: just a calendar date. Good for birthdays, deadlines, holidays.Temporal.PlainTime: just a time of day. Good for "every weekday at 9:00".Temporal.PlainYearMonth: a year and month, like"2026-04".Temporal.PlainMonthDay: a month and day with no year, like"04-24"(recurring dates).
Plus two utilities:
Temporal.Duration: an amount of time (e.g. "3 days, 4 hours").Temporal.Now: gets the current time as any of the types above.
This may seem like a lot at first, but it's actually a huge improvement because functionality is not crammed into a single object. If you just want the closest equivalent to Date, you can use ZonedDateTime. However, after reading this article, you'll have a better sense of which type is right for each use case, and you'll be able to pick the most appropriate one.
Getting the current time
Let's start with something simple. Here's how you get "right now" in the user's local time zone:
const now = Temporal.Now.zonedDateTimeISO();
// 2026-04-24T14:32:15.123+02:00[Europe/Amsterdam]
console.log(now.toString());A few things to notice. The toString() output includes the offset (+02:00) and the time zone identifier ([Europe/Amsterdam]) in square brackets. That bracketed part is specific to Temporal. It means the value doesn't just know its offset from UTC, it knows which time zone it belongs to. That matters because offsets change (daylight saving time), but zones don't.
You will get different output depending on your OS time zone.
If you only care about the date, not the time:
const today = Temporal.Now.plainDateISO();
console.log(today.toString()); // 2026-04-24And if you want an exact timestamp with no time zone attached at all, use Instant:
const instant = Temporal.Now.instant();
console.log(instant.toString()); // 2026-04-26T13:19:57.21213501ZNote: The ISO suffix on these methods (zonedDateTimeISO, plainDateISO) means "use the ISO 8601 calendar.", which is the most common calendar and the one Date uses.
Creating specific dates and times
Every Temporal type has a from() static method that accepts a string or a plain object. Strings are usually the cleanest:
const birthday = Temporal.PlainDate.from("2026-07-14");
console.log(birthday.toString()); // 2026-07-14
console.log(birthday.dayOfWeek); // 2 (Tuesday)For a zoned date-time, include the time zone in brackets:
const meeting = Temporal.ZonedDateTime.from(
"2026-05-10T14:00:00[America/New_York]",
);
console.log(meeting.toString()); // 2026-05-10T14:00:00-04:00[America/New_York]Or pass an object, which is sometimes clearer:
const deadline = Temporal.PlainDate.from({
year: 2026,
month: 12, // December (No more zero-indexed months!)
day: 31,
});
console.log(deadline.toString()); // 2026-12-31Breaking down what's happening: from() is the main entry point for creating any Temporal value. It accepts either an RFC 9557 string (the ISO 8601 format, extended with brackets for time zones and calendars) or a plain object with named fields. Months are one-indexed. December is 12, not 11.
Doing arithmetic safely
This is where Temporal really starts to pay off. Every type has add(), subtract(), since(), and until() methods, and every one of them returns a new value without touching the original.
Let's say we want to know when a 30-day trial ends:
const signupDate = Temporal.PlainDate.from("2026-04-24");
const trialEnd = signupDate.add({ days: 30 });
console.log(trialEnd.toString()); // 2026-05-24
console.log(signupDate.toString()); // 2026-04-24 (unchanged)Compare that to the Date equivalent:
const signupDate = new Date("2026-04-24");
const trialEnd = signupDate;
trialEnd.setDate(trialEnd.getDate() + 30);
console.log(trialEnd); // 2026-05-24 (good)
console.log(signupDate); // 2026-05-24 (Also changed ☹️)With Date, trialEnd and signupDate point to the same object, so mutating one mutates the other. With Temporal, add() returns a new PlainDate and leaves the original alone. This single change eliminates an entire category of bugs.
You can also go the other direction. How far apart are two dates?
const start = Temporal.PlainDate.from("2026-01-01");
const end = Temporal.PlainDate.from("2026-04-24");
const gap = start.until(end, { largestUnit: "months" });
console.log(gap.toString()); // P3M23D (3 months, 23 days)
console.log(gap.months); // 3
console.log(gap.days); // 23The result is a Temporal.Duration. That P3M23D string is ISO 8601 duration format: P for "period," 3M for 3 months, 23D for 23 days.
You may be wondering why largestUnit is needed. By default, until() returns the difference in a single unit (days), so you'd get P113D: 113 days. Passing { largestUnit: "months" } tells it to break the duration down into larger chunks.
Time zones done right
Time zones are where Date fails hardest and where Temporal shines the most.
Let's say you're scheduling a call for 3:00 PM in Amsterdam, and you want to know what time that is in New York:
const callAmsterdam = Temporal.ZonedDateTime.from(
"2026-04-24T15:00:00[Europe/Amsterdam]",
);
const callNewYork = callAmsterdam.withTimeZone("America/New_York");
console.log(callAmsterdam.toString()); // 2026-04-24T15:00:00+02:00[Europe/Amsterdam]
console.log(callNewYork.toString()); // 2026-04-24T09:00:00-04:00[America/New_York]Here's what's going on. withTimeZone() doesn't change when the meeting is — it's still the same moment in history. It changes the representation of that moment, showing it as wall-clock time in a different zone.
This is why Temporal separates Instant (a moment in time) from ZonedDateTime (a moment plus a zone): they're genuinely different concepts, and Date mashed them together.
Daylight saving time is handled correctly without you having to think about it:
// The day Amsterdam switches to summer time in 2026
const beforeDST = Temporal.ZonedDateTime.from(
"2026-03-29T01:30:00[Europe/Amsterdam]",
);
const afterDST = beforeDST.add({ hours: 1 });
console.log(afterDST.toString()); // 2026-03-29T03:30:00+02:00[Europe/Amsterdam]Notice what happened: we added 1 hour to 01:30, and we got 03:30, not 02:30. That's because the clock skipped from 02:00 to 03:00 that night. ZonedDateTime knows about the transition and does the right thing.
Immutable means safe to share
Every Temporal value is immutable, which means you can pass it around without worrying about someone else modifying it.
To change a value, you use with(), which returns a new copy:
const event = Temporal.PlainDateTime.from("2026-04-24T10:00:00");
const rescheduled = event.with({ hour: 14 });
console.log(event.toString()); // 2026-04-24T10:00:00 (unchanged)
console.log(rescheduled.toString()); // 2026-04-24T14:00:00with() is similar to the spread operator for objects: you give it the fields you want to change, and it returns a new value with everything else copied over.
Comparing values
You might think you can use < and > on Temporal values. You can, but they always throw an error (by design):
const a = Temporal.PlainDate.from("2026-01-01");
const b = Temporal.PlainDate.from("2026-06-01");
// ❌ TypeError: Do not use Temporal.PlainDate.prototype.valueOf;
// use Temporal.PlainDate.prototype.compare for comparison.
console.log(a < b);
// ❌ TypeError: Do not use Temporal.PlainDate.prototype.valueOf;
// use Temporal.PlainDate.prototype.compare for comparison.
console.log(a > b);Instead, every type has a static compare() method that returns -1, 0, or 1:
console.log(Temporal.PlainDate.compare(a, b)); // -1 (a is before b)This also makes sorting easy. The compare() method has exactly the signature that Array.prototype.sort() expects:
const dates = [
Temporal.PlainDate.from("2026-06-01"),
Temporal.PlainDate.from("2026-01-01"),
Temporal.PlainDate.from("2026-03-15"),
];
dates.sort(Temporal.PlainDate.compare);
console.log(dates.map((d) => d.toString()));
// ["2026-01-01", "2026-03-15", "2026-06-01"]For equality, use equals():
const x = Temporal.PlainDate.from("2026-04-24");
const y = Temporal.PlainDate.from("2026-04-24");
console.log(x.equals(y)); // true
console.log(x === y); // false (different objects)Browser support and the polyfill
Temporal ships natively in Firefox 139+ (since May 2025) and Chrome 144+ (since January 2026), as well as Edge and Deno. Safari doesn't support it yet. It's in Safari Technical Preview behind a flag, but not available to users in the stable release.
Because of that, you'll want a polyfill for production use today. There are two good options:
@js-temporal/polyfill: maintained by the proposal champions.temporal-polyfill: a smaller, faster alternative by the FullCalendar team.
Usage is the same either way:
import { Temporal } from "@js-temporal/polyfill";
const today = Temporal.Now.plainDateISO();Depending on your project, you can ship the polyfill to all users, or use feature detection to load it only when needed:
if (typeof Temporal === "undefined") {
// load the polyfill
}Don't let the Safari gap stop you from adopting Temporal as the polyfills are production-ready and are what the spec was tested against. You'll be able to drop them once Safari ships native support.
Conclusion
Temporal is the biggest change to JavaScript date handling in thirty years. It replaces a single confused Date object with a family of purpose-built, immutable types, and it handles time zones, calendars, and arithmetic the way you'd expect a modern API to.
Should you rewrite all your existing Date code right now? Probably not. Date isn't going anywhere, and the two APIs interoperate cleanly (date.toTemporalInstant() converts a legacy Date straight to a Temporal.Instant, and every Temporal type has an epochMilliseconds property to go the other way). But for any new date logic, Temporal is the clear choice.
For a quick side-by-side reference you can come back to, I've put together a Temporal cheatsheet that covers the most common Date vs Temporal patterns. And for going deeper, the full reference on MDN has a page per type with every method documented.




