Temporal vs date-fns in 2026. Which Should You Use?
📧 Subscribe to JS Pulse
The JavaScript job market in one email: real numbers from 600+ job listings, 5 jobs worth applying to, and what actually changed. Join 9,000+ developers.
Updated October 2026.
Should you replace date-fns with Temporal in 2026? For new code that deals with time zones, I would start with Temporal today. For an existing app where date-fns works, I would not rip it out just to delete a dependency. And if a real share of your users are on iPhones, you still need a polyfill, because Safari has not shipped Temporal yet.
That is the short answer. The rest of this guide is the long one, with code you can run.
Most articles about Temporal say date libraries are dead. I wrote one of those myself earlier this year. I was wrong about the timing. Temporal is the better foundation for dates in JavaScript, but "better foundation" and "delete date-fns on Monday" are two different decisions. One is about the language. The other is about your users, your codebase, and how much risk you want this sprint.
I look at a lot of JavaScript job listings. On my board, 71% of the listings posted in 2026 ask for TypeScript, up from 37% in 2025. That number is based on 587 tagged listings since March 2025, so it is one board, not the whole market. But it tells you where teams are spending money. They pay for code that catches mistakes before production. Temporal fits that direction better than any date library, because its types make the difference between a calendar date and a moment in time impossible to ignore.
Temporal vs date-fns at a Glance
Here is the comparison I would want before making the call.
| What you care about | Temporal | date-fns v4 | Moment.js |
|---|---|---|---|
| Part of JavaScript itself | Yes | No, npm package | No, npm package |
| Values | Immutable Temporal types | Native Date objects, functions return new values | Mutable moment objects |
| Time zones | Built in with ZonedDateTime | Through @date-fns/tz | Through moment-timezone |
| Daylight saving control | Explicit disambiguation options | Through @date-fns/tz | Through moment-timezone |
| Custom format tokens like yyyy-MM-dd | No, formatting goes through Intl | Yes | Yes |
| Business day helpers | Write your own | Built in, for example addBusinessDays | Plugins or your own code |
| Works natively in Safari | No, needs a polyfill | Yes, it is plain JavaScript | Yes, it is plain JavaScript |
| Extra code in your bundle | None where native, the polyfill elsewhere | Only the functions you import | The whole library plus any locales |
| Project status | Standard JavaScript API | Actively maintained | Maintenance mode |
The table is honest on purpose. Temporal wins on correctness and on time zones. date-fns still wins on formatting tokens and ready made helpers. Moment.js only wins on being already installed in old projects.
Where Temporal Runs Today
Chrome 144 shipped Temporal on January 13, 2026, and Edge 144 followed. Firefox got there first. Temporal has been on by default since Firefox 139 in May 2025.
On the server, Node.js 26 was released on May 5, 2026 with Temporal enabled by default, no flag needed. Node 26 is scheduled to become the LTS line in October 2026, which is the moment many teams will actually upgrade. Bun and Deno already offer Temporal too. If you are choosing a runtime right now, I compared them in my Node.js, Bun and Deno runtime comparison.
Safari is the gap. At the time of this update, stable Safari does not ship Temporal, and every browser on iOS runs on Safari's engine. So any public website that wants to use Temporal in the browser needs a fallback. Check the current status on caniuse.com before you ship, because this is the line in this article most likely to change.
The fallback is the official polyfill. In an ES module you can use the native API when it exists and load the polyfill only when it does not.
// npm install @js-temporal/polyfill
const Temporal =
globalThis.Temporal ??
(await import('@js-temporal/polyfill')).Temporal;
export { Temporal };
Import Temporal from that one module everywhere in your app. When Safari ships native support, you delete one line instead of touching every file.
When I Would Choose Temporal
I would pick Temporal for any new code that has to answer the question "what time is this for the person reading it". Scheduling, bookings, reminders, anything with users in more than one country. That is where the old Date object hurts the most and where Temporal is clearly better.
I would also pick it for anything on the server running Node 26 or newer. There is no polyfill cost there and no Safari problem. A backend that stores instants and converts them to local time is exactly what Temporal was designed for.
And I would pick it when the bug you keep fixing is "this was the wrong day". Temporal separates a calendar date from a moment in time at the type level. You cannot accidentally treat one as the other, and that alone removes a category of off by one bugs.
When I Would Keep date-fns
If your app formats dates with custom patterns everywhere, keep date-fns for now. Temporal formats through Intl, which is great for localized output and annoying when a design asks for one exact pattern in one exact place.
If you depend on helpers like addBusinessDays, keep date-fns. Temporal gives you the building blocks, not the business rules. You would be writing and testing those helpers yourself.
If you have a large codebase where date-fns works and nobody is filing date bugs, keep it. A migration that only removes a dependency rarely survives contact with the roadmap. I would migrate a module when I am already touching it, not as a project of its own.
The authors of date-fns make a similar point themselves. Temporal replaces a lot of what date-fns does, not all of it. The two can live in the same app while you move over.
The Types That Make Temporal Worth It
The old Date object tries to be a calendar date, a clock time, a timestamp, and a local time all at once. Temporal splits those into separate types. This is the part that actually changes how you write code.
// A date on a calendar, no time, no time zone
const launch = Temporal.PlainDate.from('2026-03-15');
console.log(launch.month); // 3 (January is 1)
console.log(launch.dayOfWeek); // 7 (Sunday)
// A time of day, no date
const standup = Temporal.PlainTime.from('09:30');
console.log(standup.add({ minutes: 90 }).toString()); // "11:00:00"
// A local date and time, no time zone
const local = Temporal.PlainDateTime.from('2026-03-15T14:30');
// A real moment in a real place
const meeting = Temporal.ZonedDateTime.from(
'2026-03-15T14:30[America/New_York]'
);
console.log(meeting.toString());
// "2026-03-15T14:30:00-04:00[America/New_York]"
console.log(meeting.withTimeZone('Asia/Tokyo').toString());
// "2026-03-16T03:30:00+09:00[Asia/Tokyo]"
// An exact point on the timeline, for storage and APIs
const instant = meeting.toInstant();
console.log(instant.toString()); // "2026-03-15T18:30:00Z"
// A length of time
const sprint = Temporal.Duration.from({ weeks: 2 });
console.log(launch.add(sprint).toString()); // "2026-03-29"
Every one of these objects is immutable. add() returns a new value and leaves the original alone. If you write launch.add({ days: 7 }) and do not assign the result, nothing happens. That feels strange for a day and then it feels obvious.
The Daylight Saving Example That Sold Me
This is the clearest case for Temporal I know. In New York, clocks moved forward on March 8, 2026. Look at what "one day later" and "24 hours later" mean across that change.
const start = Temporal.ZonedDateTime.from(
'2026-03-07T12:00-05:00[America/New_York]'
);
const nextDay = start.add({ days: 1 });
const after24Hours = start.add({ hours: 24 });
console.log(nextDay.toString());
// "2026-03-08T12:00:00-04:00[America/New_York]"
console.log(after24Hours.toString());
// "2026-03-08T13:00:00-04:00[America/New_York]"
Both answers are correct. They answer different questions. A daily reminder wants the first one. A 24 hour countdown wants the second. With the old Date object you get one behavior and a bug report later. With Temporal you choose.
The same control exists for times that do not exist at all. On March 8, 2:30 AM never happened in New York.
try {
Temporal.ZonedDateTime.from(
'2026-03-08T02:30[America/New_York]',
{ disambiguation: 'reject' }
);
} catch (error) {
console.log('2:30 AM does not exist on this day');
}
// The default, 'compatible', moves it forward instead
const adjusted = Temporal.ZonedDateTime.from(
'2026-03-08T02:30[America/New_York]'
);
console.log(adjusted.toString());
// "2026-03-08T03:30:00-04:00[America/New_York]"
date-fns and Temporal Side by Side
If you already know date-fns, these are the translations you will use most. I kept the variable names different so you can paste each pair into one file.
import { addDays, differenceInDays, format, isAfter } from 'date-fns';
// Add days
const fnsDeadline = addDays(new Date(2026, 9, 10), 7); // month 9 is October
const tDeadline = Temporal.PlainDate.from('2026-10-10').add({ days: 7 });
console.log(tDeadline.toString()); // "2026-10-17"
// Difference in days
const fnsDays = differenceInDays(new Date(2026, 11, 31), new Date(2026, 0, 1));
const tDays = Temporal.PlainDate.from('2026-01-01')
.until(Temporal.PlainDate.from('2026-12-31')).days;
console.log(fnsDays, tDays); // 364 364
// ISO date string
const fnsIso = format(new Date(2026, 2, 15), 'yyyy-MM-dd');
const tIso = Temporal.PlainDate.from('2026-03-15').toString();
console.log(fnsIso, tIso); // "2026-03-15" "2026-03-15"
// Comparison
const a = Temporal.PlainDate.from('2026-04-01');
const b = Temporal.PlainDate.from('2026-03-15');
console.log(isAfter(new Date(2026, 3, 1), new Date(2026, 2, 15))); // true
console.log(Temporal.PlainDate.compare(a, b) > 0); // true
Notice the types. date-fns gives you back a Date. Temporal gives you back a PlainDate. They are not interchangeable, and that is the point. A PlainDate cannot quietly shift by a day because of the server's time zone.
For human readable output, Temporal hands the job to Intl.
const day = Temporal.PlainDate.from('2026-03-15');
console.log(day.toLocaleString('en-US', { dateStyle: 'long' }));
// "March 15, 2026"
console.log(day.toLocaleString('de-DE', { dateStyle: 'long' }));
// "15. März 2026"
This is better for localized apps and worse for one off custom patterns. That trade off is the main reason I still keep date-fns around in some projects.
Time zones are where the gap used to be widest. date-fns v4 closed a lot of it with the @date-fns/tz package and its time zone aware date class. It works. It is still a library layered on top of Date, while in Temporal the time zone is part of the value itself.
Living With Old Date Code
No real migration happens in one commit. You will have Date objects coming from libraries, ORMs and APIs for a long time. Converting at the boundary is simple.
// Date to Temporal
const legacy = new Date('2026-10-10T12:00:00Z');
const asInstant = Temporal.Instant.fromEpochMilliseconds(legacy.getTime());
// Temporal back to Date
const backToDate = new Date(asInstant.epochMilliseconds);
One detail matters here. Temporal keeps nanoseconds and Date keeps milliseconds, so going back to Date drops anything below a millisecond. For almost every app that is fine. For logs and ordering events it is worth knowing.
A Real Scheduling Function
Older Temporal tutorials, including the first version of this one, used a method called getISOFields() to build zoned dates. That method was removed from the final API. If you copied that pattern, it will break. This is the version I use now.
function scheduleMeeting(dateString, timeString, hostTimeZone) {
const date = Temporal.PlainDate.from(dateString);
const time = Temporal.PlainTime.from(timeString);
const meeting = date
.toPlainDateTime(time)
.toZonedDateTime(hostTimeZone, { disambiguation: 'reject' });
return {
instant: meeting.toInstant(),
inZone: (zone) => meeting.withTimeZone(zone).toString()
};
}
const call = scheduleMeeting('2026-03-15', '14:00', 'America/New_York');
console.log(call.inZone('Europe/London'));
// "2026-03-15T18:00:00+00:00[Europe/London]"
console.log(call.inZone('America/Los_Angeles'));
// "2026-03-15T11:00:00-07:00[America/Los_Angeles]"
I use disambiguation: 'reject' on purpose. If someone books a meeting at a time that does not exist because of a clock change, I want an error I can show them, not a meeting that silently moved by an hour.
Validating a Date From a Form
A common mistake is to treat "Temporal parsed it" as "the date is valid". Temporal has an overflow option, and the safe way to validate user input is to say exactly what you want.
function isValidIsoDate(value) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
const [year, month, day] = value.split('-').map(Number);
try {
Temporal.PlainDate.from({ year, month, day }, { overflow: 'reject' });
return true;
} catch {
return false;
}
}
console.log(isValidIsoDate('2026-02-28')); // true
console.log(isValidIsoDate('2026-02-29')); // false, 2026 is not a leap year
console.log(isValidIsoDate('2026-13-01')); // false
Compare that to new Date(2026, 1, 29), which quietly gives you March 1. That one behavior has caused more support tickets than I want to count.
Storing Temporal Values
Store moments as instants and calendar dates as plain dates. Do not store the full ZonedDateTime string, because most databases do not understand the bracketed time zone at the end.
const createdAt = Temporal.Instant.from('2026-02-19T12:00:00Z');
console.log(createdAt.toString()); // "2026-02-19T12:00:00Z"
console.log(createdAt.epochMilliseconds); // 1771502400000
const birthday = Temporal.PlainDate.from('1990-06-15');
console.log(birthday.toString()); // "1990-06-15"
// Reading it back for a specific user
const forUser = Temporal.Instant.from('2026-02-19T12:00:00Z')
.toZonedDateTimeISO('Europe/Berlin');
console.log(forUser.toString());
// "2026-02-19T13:00:00+01:00[Europe/Berlin]"
In PostgreSQL that means TIMESTAMPTZ for instants and DATE for plain dates. If you need to rebuild a zoned value later, store the time zone name in its own column.
Mistakes I Keep Seeing
The first is using PlainDateTime for something that happens at a real moment. A flight, a deadline, a webinar. PlainDateTime has no idea where it is, so converting it later during a clock change gives you the wrong hour. Use ZonedDateTime for anything that happens somewhere.
The second is asking an Instant what hour it is. An Instant has no hour, because the hour depends on where you stand. Convert it with toZonedDateTimeISO() first.
The third is forgetting that until() measures the gap between dates, not the number of days in a range. From July 1 to July 14 is 13 days apart, but a booking that covers both ends is 14 days long. Name your variables so the next person knows which one you meant.
The fourth is monthly recurrence. Add one month to January 31 and you get February 28 in 2026, because Temporal constrains the day by default. That is usually right for billing and usually wrong for "the last day of every month". Decide which rule you want before you write the loop.
The fifth is leftover month + 1 code from the Date era. Temporal months start at 1. If you migrate a file and keep the old adjustment, every date moves forward by a month.
Temporal in React and Next.js
On the server, the question is your Node version. Node 26 has Temporal built in, so Server Components and API routes can use it directly. On older Node versions, import from the same polyfill module I showed earlier.
On the client, the question is Safari again. Anything that runs in the browser needs the fallback until Safari ships. I would keep date logic on the server where possible, send instants as ISO strings, and convert to the user's time zone at the last step with Temporal.Now.timeZoneId(). If you are building on the App Router, I covered the server and client split in my guide to Next.js production applications.
Temporal also ships with solid TypeScript definitions. PlainDate, ZonedDateTime and Instant are distinct types, so passing one where another is expected fails at build time. If your team leans on the type system the way most job listings now expect, my notes on advanced TypeScript patterns for React pair well with this.
How I Would Migrate a Real App
I would start by adding the polyfill module and writing all new date code with Temporal. That costs nothing and stops the old pattern from spreading.
Next I would convert at the boundaries. Dates coming from the database and APIs become instants or plain dates as soon as they enter my code, and become Date objects again only where an old library demands it.
Then I would move the date utility functions one by one, keeping their signatures so callers do not change. Time zone logic first, because that is where Temporal pays back fastest. Custom formatting last, because that is where date-fns is still the better tool.
Before every release I would test in Safari on a real iPhone. That is the environment where a Temporal migration is most likely to surprise you, and the one most developers forget to open.
Questions People Ask Me
Is Temporal better than date-fns?
For time zones, daylight saving rules and keeping dates and moments apart, yes. For custom format patterns and ready made business helpers, date-fns is still more convenient. In 2026 the best answer for many apps is both, with Temporal growing and date-fns shrinking.
Can Temporal fully replace date-fns?
Not in every app. Temporal replaces most date math. You will write some helpers yourself, and formatting moves to Intl.
Does Temporal work in Safari?
Not natively in stable Safari at the time of this update. Use the official polyfill for browser code and check caniuse.com for the current status.
Is Temporal available in Node.js?
Yes. It is enabled by default starting with Node.js 26. Older versions need the polyfill.
Should I still start new projects with Moment.js?
No. Moment.js is in maintenance mode and its own maintainers recommend moving to modern alternatives. For new code I would choose Temporal, with date-fns for the gaps.
The Real Decision
JavaScript finally has a date API that models time correctly. That part of the hype is true. What the hype skips is that your users do not run the language spec. They run Chrome, Firefox, an iPhone, and whatever Node version your platform team approved last year.
So I would not frame this as Temporal against date-fns. I would frame it as a direction. New code moves to Temporal. Old code moves when you touch it. date-fns stays where it is still the better tool, until it is not.
The best date library is still the one your users' devices can run.
I track what JavaScript teams actually ask for in their job listings at jsgurujobs.com. If you want to see which of these skills companies are hiring for right now, start with the open TypeScript developer jobs and Node.js developer jobs on the board.
📧 Subscribe to JS Pulse
The JavaScript job market in one email: real numbers from 600+ job listings, 5 jobs worth applying to, and what actually changed. Join 9,000+ developers.
Share this article