On this page · 11 sections
Summary. Node.js 26.0.0 shipped on May 5, 2026 with the Temporal API enabled by default, no flag required, alongside V8 14.6 and Undici 8.0. Temporal is the modern replacement for the Date object, and it reached TC39 Stage 4 on March 13, 2026, which locks it into the ECMAScript 2026 standard. It brings immutable date and time types, first-class IANA time zones and calendars, and nanosecond precision, and it ships with around 4,500 conformance tests against Date's 594. You do not rewrite everything at once: the migration is incremental, bridged by Date.prototype.toTemporalInstant(), and older runtimes can load a polyfill. Node 26 also carries breaking changes unrelated to dates, so check those before you bump the version. This guide gives the types you will actually use, a Date-to-Temporal migration map with code, the gotchas that bite, and the upgrade checks to run first.
Date has been JavaScript's weak spot since the language shipped. Philip Chimento and Ujjwal Sharma of Igalia co-championed the fix, and the firm's Stage 4 announcement is blunt: Temporal "replaces JavaScript's Date API, which has badly underserved developers since 1995." Getting date logic right is worth the effort, because the failure mode is silent: an off-by-one-day or a mishandled offset corrupts data long before anyone notices. Teams that add real timezone and calendar coverage grow their CI matrices, and at GitHub Actions' $0.006 per Linux minute in 2026 those runs are not free, so it pays to migrate once and correctly. Node 26 removes the last excuse, because Temporal is now in the runtime with no dependency to install.
What Temporal fixes
The Date object has four structural problems Temporal removes. Date is mutable, so setMonth changes the object in place and aliasing bugs follow. Date has no concept of a time zone beyond the host's local offset and UTC, so anything involving another zone means manual math or a library. Date conflates a wall-clock date, a wall-clock time and an exact instant into one type, so "a birthday" and "a precise timestamp" share the same broken API. And Date parsing is famously loose.
Temporal splits those concerns into explicit, immutable types. An exact moment is a Temporal.Instant with nanosecond precision. A moment in a specific zone is a Temporal.ZonedDateTime, which carries an IANA zone and a calendar. A calendar date with no zone is a Temporal.PlainDate. Arithmetic returns a new value rather than mutating the old one. The maturity shows in the test suite: Temporal ships with roughly 4,500 test262 conformance tests, where Date has 594. Firefox 139, Chrome 144, Edge 144 and the TypeScript 6.0 beta already include it, and now so does Node.js 26.
The Temporal types you will actually use
Most migrations touch five or six types. Pick the type by the question "does this value have a time zone, and does it have a time-of-day?" rather than reaching for one catch-all.
| Type | Represents | Use instead of |
|---|---|---|
Temporal.Instant |
An exact moment, nanosecond precision | Date for timestamps and logging |
Temporal.ZonedDateTime |
An instant in a named IANA zone plus calendar | Date with manual offset math |
Temporal.PlainDate |
A calendar date, no time or zone | Date for birthdays, due dates, invoices |
Temporal.PlainTime |
A wall-clock time, no date | ad-hoc "HH:mm" string handling |
Temporal.PlainDateTime |
A date and time with no zone | Date used as a "local" datetime |
Temporal.Duration |
A length of time | manual millisecond arithmetic |
The rule of thumb: store and compare exact moments as Instant, do human-facing calculations in ZonedDateTime, and use PlainDate for anything that is a date on a calendar rather than a point on the world clock.
A Date-to-Temporal migration map
You can migrate file by file. The table below is the core mapping; the code that follows shows each pattern in context.
| Task | Legacy Date | Temporal |
|---|---|---|
| Current timestamp | Date.now() |
Temporal.Now.instant() |
| Current wall clock in a zone | new Date() plus a library |
Temporal.Now.zonedDateTimeISO('Asia/Kolkata') |
| Parse a calendar date | new Date('2026-01-15') |
Temporal.PlainDate.from('2026-01-15') |
| Add six months | manual month math | date.add({ months: 6 }) |
| Difference between dates | millisecond subtraction | a.until(b, { largestUnit: 'day' }) |
Bridge an existing Date |
not applicable | existing.toTemporalInstant() |
Timestamps and the bridge from Date
For a precise moment, replace Date.now() with Temporal.Now.instant(). When you receive a Date from an older library or a database driver, convert it with the toTemporalInstant method added to Date.prototype:
// Old
const ts = Date.now(); // number of milliseconds
// New
const now = Temporal.Now.instant(); // nanosecond-precision Instant
console.log(now.epochMilliseconds); // interop with millisecond APIs
// Bridge a Date you were handed
const legacy = new Date('2026-01-15T09:30:00Z');
const asInstant = legacy.toTemporalInstant();
const inKolkata = asInstant.toZonedDateTimeISO('Asia/Kolkata');
console.log(inKolkata.toString()); // 2026-01-15T15:00:00+05:30[Asia/Kolkata]
Calendar dates and arithmetic
Use PlainDate for values that are dates, not instants, such as an invoice date or a date of birth. Arithmetic is immutable and reads like the intent:
const due = Temporal.PlainDate.from('2026-01-15');
const nextCycle = due.add({ months: 6, days: 15 }); // 2026-07-30, new value
const daysLeft = Temporal.Now.plainDateISO().until(due, { largestUnit: 'day' });
console.log(daysLeft.days);
// Comparisons are explicit, no < or > coercion
if (Temporal.PlainDate.compare(nextCycle, due) > 0) {
// nextCycle is later
}
Wall-clock work in a real zone
ZonedDateTime does the offset and daylight-saving math for you. Ask for "9am next Monday in New York" without touching an offset table:
const meeting = Temporal.ZonedDateTime.from({
year: 2026, month: 3, day: 9, hour: 9, minute: 0,
timeZone: 'America/New_York',
});
const inIndia = meeting.withTimeZone('Asia/Kolkata');
console.log(inIndia.toString()); // same instant, +05:30 wall clock
Serialization and formatting
Temporal serializes to strings that keep the zone and calendar, using the RFC 9557 IXDTF format (for example a trailing [Asia/Kolkata] annotation). Store the string, parse it back losslessly, and format for humans with Intl:
const zdt = Temporal.Now.zonedDateTimeISO('Asia/Kolkata');
const stored = zdt.toString(); // round-trippable, keeps the zone
const back = Temporal.ZonedDateTime.from(stored);
const label = back.toLocaleString('en-IN', {
dateStyle: 'medium', timeStyle: 'short',
});
Persist an exact moment as either the IXDTF string or instant.epochNanoseconds, and reconstruct with Temporal.Instant.from(...). Do not store a formatted local string as your source of truth.
Gotchas that will bite
The most common mistake is reaching for PlainDateTime when you mean ZonedDateTime. A PlainDateTime has no zone, so "March 9 at 2am" is not a real instant until you attach a zone, and in a spring-forward transition that wall-clock time may not exist at all. If a value must map to a point on the world clock, use ZonedDateTime or Instant.
Two more that catch teams. First, do not quietly convert Temporal values back to Date at the edges and lose precision or zone information; keep them as Temporal all the way to the boundary and convert only when an external API demands a Date. Second, when you serialize across calendar systems, keep the calendar annotation ([u-ca=hebrew] and similar) that RFC 9557 defines, or round-tripping a non-ISO calendar date will drop information. Our interop 2026 web platform guide covers the broader pattern of adopting new platform primitives without breaking the boundaries of your system.
Before you upgrade to Node.js 26
Temporal is the headline, but Node 26 is a major release with removals that can crash an app on the first restart. Audit these before you bump the version, not after.
| Change | Impact | Fix |
|---|---|---|
http.writeHeader() removed |
Middleware calling it crashes on startup | Switch to writeHead() |
Legacy _stream_readable / _stream_transform removed |
Import-time failures | Use require('stream').Readable, Writable and pipeline |
| Readable streams read one buffer at a time | Throughput and backpressure behaviour shifts | Re-test stream consumers under load |
customization-hooks loader runtime-deprecated |
DeprecationWarning, removal later |
Plan the loader migration now |
localStorage returns undefined with no persistence file |
Tests relying on in-memory state break | Configure persistence or mock it |
| Build: GCC 13.2, no Python 3.9 | Native addons fail to compile | Rebuild native addons for the new module version |
The riskier the jump, the more this matters: an app that skipped upgrades since Node 22 will likely hit several of these at once. Run npm outdated and look for packages untouched since Node 16, which are the ones most likely to reach into removed internals. This is the same "evaluate on Current, promote at LTS" discipline we describe in our note on Node.js moving to one release per year; Node 26 becomes LTS in October 2026.
Older runtimes and cross-environment code
If part of your stack runs on a runtime without Temporal yet, load a polyfill and keep one API across environments. The lightweight temporal-polyfill from FullCalendar is about 20 KB, and the reference @js-temporal/polyfill maintained by the champions is about 44 KB. Because Temporal is Stage 4 and already in Firefox 139, Chrome 144, Edge 144 and Node 26, the polyfill is a shrinking, temporary cost rather than a permanent dependency. Teams already running native TypeScript workflows through Node's native type stripping or weighing the TypeScript 7 migration can fold the Temporal move into the same toolchain pass.
India-specific considerations
For teams building out of India, the Asia/Kolkata zone is a live test of your date code, because its offset is a half hour, +05:30, and a surprising amount of naive code assumes whole-hour offsets. Temporal handles it correctly through the IANA database with no special casing. The bigger win is multi-zone scheduling: product teams serving both India and the US spend real engineering time reconciling +05:30 against US zones that observe daylight saving, and ZonedDateTime.withTimeZone does that conversion without an offset table. For services where date correctness underpins money, such as billing, payroll and settlement windows, moving that logic onto explicit Temporal types removes a class of silent errors before it reaches production.
Bottom line
Node.js 26 makes Temporal a runtime default, so the migration is now a code change rather than a dependency decision. Move exact moments to Instant, human-facing math to ZonedDateTime, and calendar values to PlainDate, bridging existing Date objects with toTemporalInstant() so you can go file by file. Audit the Node 26 breaking changes before you upgrade, keep a polyfill only where a runtime still lacks Temporal, and store dates as IXDTF strings or epoch nanoseconds rather than formatted local text. The payoff is a whole category of timezone and mutation bugs that simply stops happening.
FAQ
How eCorpIT can help
eCorpIT is a CMMI Level 5 and ISO 27001:2022 certified engineering organisation in Gurugram, and our senior-led teams run Node.js upgrades and date-handling migrations on production systems. We can audit a codebase for the Node 26 breaking changes, plan an incremental Date-to-Temporal move with tests around your timezone and calendar logic, and keep a polyfill only where a runtime still needs it. If you want a Node 26 and Temporal migration plan, talk to our engineering team.
References
_Last updated: July 28, 2026._