Node.js 26 Enables Temporal by Default - Time to Ditch Date?
What happened - Node 26 just "turned Temporal on"
First, the facts. I measured on the current latest, Node v26.4.0 (2026-06-24).
node v26.4.0 | Temporal global? true
v8 14.6.202.34-node.21 | undici 8.5.0 | npm 11.17.0
That single line - typeof Temporal returning something other than undefined - is where this post begins. Temporal was a long-ripening proposal at TC39 (the JavaScript standards committee), and Node 26, by bumping its V8 engine to 14.6, promoted it to exposed by default, no flag required (per the official release notes, #61806). Browsers are following the same V8 lineage, so Temporal's status has shifted from "a standard someday" to "something now baked into the runtime."
This is also part of a broader pattern across today's runtimes. Much like Python 3.15 recently switching the default encoding for text I/O to UTF-8, more and more languages and runtimes are biting the bullet and replacing "a bad default they'd tolerated for years" in a major release. Turning Temporal on by default is a decision cut from the same cloth.
Wait, what is Temporal?
Let's start with an analogy. Date is an all-in-one Swiss Army knife that crams everything onto a single blade. "This exact moment" is a Date, "my birthday, June 29" is a Date, "the meeting is at 3 PM Seoul time" is also expressed with that same one Date. The trouble is that these three are really completely different kinds of concept. A birthday should carry no time zone, "this exact moment" is a single point that's identical everywhere on Earth, and "3 PM in Seoul" only means anything with a time zone attached. When one type has to be all three, something is always slightly off.
Temporal splits that knife into a toolbox with a dedicated tool for each job. Boiled down to the core types, it looks like this:
| Temporal type | What it represents | Everyday analogy |
|---|---|---|
Instant |
An absolute point in time with no zone (one instant the whole world shares) | The exact shutter moment frozen in a photo |
PlainDate |
A date with no time and no time zone | A day circled on a calendar |
PlainDateTime |
A date and time with no time zone | A note that just says "3 PM" |
ZonedDateTime |
A complete moment with the time zone attached | A plane ticket reading "3 PM Seoul" |
Duration |
A span of time (like 2 days 3 hours) | Elapsed time on a stopwatch |
You might wonder, "Why split it this finely?" The next few sections are the answer. Once it's split, the things a single Date used to get quietly wrong either can't be mixed up in the first place, or blow up as an error the moment they are.
Why Date has taken 30 years of flak
Date is an API copied wholesale from Java's already-dated Date back in the earliest days of JavaScript, in 1995. Those fingerprints are still there, still tripping people up. I ran three of them myself.
1) Months count from zero
A human writes July as 7, but Date writes it as 6.
new Date(2026, 6, 29)
// => Wed Jul 29 2026 // put in 6, get July
Temporal {month: 7}
// => 2026-07-29 // put in 7, get July (1-based)
2) Values change out from under you (mutability)
With Date, methods like setMonth modify the original in place. Hand a Date off somewhere, and if another function changes it, your own variable changes too.
Date.setMonth(+1) โ original mutates to 2026-08-29 // share it and it's a bug
Temporal.add(+1m) โ keeps original 2026-07-29, returns new 2026-08-29 // immutable
3) String parsing is interpreted differently depending on the format
Write the same date slightly differently, and the time-zone interpretation diverges.
new Date("2026-06-29")
// => 2026-06-29T00:00:00.000Z // UTC midnight
new Date("2026/06/29")
// => Mon Jun 29 2026 00:00 GMT+0900 // local midnight (different!)
Temporal.PlainDate.from("2026-06-29")
// => 2026-06-29 // no concept of a time zone at all
You only swapped a - for a /, yet the result is off by nine hours. This kind of thing rarely gets caught in code review.
The real trap is time zones
You can memorize your way around those three. But the billing incident from the opening - daylight saving time (DST) - is hard to dodge by memorization. This is where the most expensive bugs in Date-based time handling come from.
DST is the practice of moving clocks forward an hour in spring (making that day 23 hours) and back an hour in fall (25 hours). Because of it, "one calendar day later" and "physically 24 hours later" can land on different days. I compared them directly at the 2026 US Eastern spring transition (March 8).
start : 2026-03-07T12:00:00-05:00[America/New_York]
+1 day (calendar) : 2026-03-08T12:00:00-04:00 // still 12:00 midday - absorbs the DST shift
+24h (physical) : 2026-03-08T13:00:00-04:00 // 13:00 - pushed an hour later
Date +24h (ms) : 13:00 once converted to Eastern // Date only adds ms, so it's blind to DST
On Temporal.ZonedDateTime, add({ days: 1 }) understands "a day as people mean it." So even across the transition, the clock still reads 12:00. add({ hours: 24 }), on the other hand, is 24 hours of physical time, so it slides to 13:00. Both are correct. The point is that in Temporal you write down explicitly which of the two you want. Date has nothing but millisecond arithmetic, so only the latter (physical 24 hours) is ever possible - and when you meant "one calendar day," as in that billing batch, it fails silently.
This is why the types are split. With a zone-less Instant, you can't add "one calendar day" in the first place - knowing about DST requires knowing which time zone you're in (ZonedDateTime). Temporal enforces that distinction through the type system.
Diagram: this section is illustrated with a flowchart - view it in the original article on var.gg.
Calendar arithmetic: refusing instead of quietly getting it wrong
Some date calculations don't have a single clean answer. What's "one month after January 31"? There is no February 31. Date rolls it over into March without a word.
Date.setMonth(+1)(Jan 31)
// => Tue Mar 03 2026 // skips February, overflows into March
Temporal.add(+1m)
// => 2026-02-28 // clamped to Feb's last day (constrain)
Temporal.add(+1m, reject)
// => RangeError // rejects it: "there is no Feb 31"
By default (constrain), Temporal clamps to the last day of February; but hand it { overflow: 'reject' } and it throws an error instead of quietly patching up an invalid date. In places like accounting and settlement, where "when in doubt, stop" is the right rule, this difference is decisive. Date has no notion of "reject" at all.
So can you ditch Date? - an honest look at the limits
By this point Temporal might look like a cure-all, but this isn't a call to rip everything out today. There are honest limits, too.
- The ecosystem is still catching up. Libraries like
date-fns,Luxon, andDay.js, along with mountains of existing code, still passDateobjects around. At the boundaries you'll need conversions liketoPlainDate()/Temporal.Instant.fromEpochMilliseconds(), and ORMs, DB drivers, and JSON serialization often don't understand Temporal types out of the box. Dateisn't going away. Temporal is an option that replacesDate, not one that removes it. Existing code keeps running as-is. "New code from here on uses Temporal" is about as realistic as it gets.- Backward compatibility. If you have to support Node below 26, or browsers that haven't enabled Temporal yet, you'll need a polyfill - and the polyfill isn't small.
- The learning curve. Splitting into five types means dropping the "just use a
Datefor anything" habit. At first you'll hesitate every time over whether to reach forPlainDateTimeorZonedDateTime. But that hesitation is precisely what Temporal intends -Datebred bugs by letting you skip the question entirely.
In short, Temporal is not a tool that makes things shallowly easier; it's one that makes things correct the hard way. You pay an entry cost, and in return it takes away the chronic pain of time-zone and calendar bugs.
Things to check before upgrading to Node 26
Before you jump to Node 26 for Temporal alone, you should account for the compatibility changes that rode in with this major. Here's what I verified firsthand.
Removed legacy APIs
http.ServerResponse.prototype.writeHeader (use writeHead instead) and the old internal stream modules (_stream_wrap and friends) are gone for good. require('_stream_wrap') now genuinely dies with MODULE_NOT_FOUND. If an aging dependency references it, your upgrade stalls right there.
The --experimental-transform-types flag is removed, module.register() is runtime-deprecated, and the native add-on ABI version (NODE_MODULE_VERSION) rose to 147 - compiled native modules need a rebuild. On the build side there's also a bump to GCC 13.2 or newer and the end of Python 3.9 support.
Nice extras that came along for the ride
Because it's V8 14.6, you also get Map.prototype.getOrInsert / getOrInsertComputed (one-shot methods that fetch if present and insert if not) and Iterator.concat. I confirmed all three actually work.
The timeline
Node 26 is promoted to LTS in October 2026, and stays Current until then. And starting with Node 27 the release model changes: one major per year (in April), with all of them LTS (InfoQ, 2026-06). In other words, 26 is the last branch of the old model, and Temporal is the biggest change riding on that final branch.
For broader context on where the JS runtime ecosystem has been heading lately, I've also covered it separately in pieces on Deno 2.8's cleanup of its npm-compatible toolchain and TypeScript's move to a native compiler (tsgo).
Wrapping up - when to use Temporal
Here's my conclusion after swapping it in and using it myself.
- For any new time or date logic, make Temporal the default. Especially code where "a silent one-hour or one-day drift leaks money" - time zones, DST, recurring schedules, settlement cutoffs.
- Don't rewrite a huge existing
Datecodebase all at once - start at the boundaries (input parsing, output formatting) and move inward gradually. Converting just the inner calculations to Temporal already makes most of the bugs vanish. - For a log that just stamps "the current time" once,
Dateis perfectly fine. Cramming Temporal into every last corner is its own kind of overkill.
The Date that was hastily copied over 30 years ago now has a successor inside the standard runtime. Node 26 turning it on without a flag is the signal that "migrate someday" has become "evaluate now." If you'd rather not be woken at dawn by the next time-zone bug, I'd start with that one line - the one where typeof Temporal comes back true.
Originally published at var.gg.
Comments
No comments yet. Start the discussion.