Skip to content

Web Development · JavaScript

Migrating From Date to Temporal: A Practical JavaScript Guide

Temporal reached TC39 Stage 4 in March 2026 and ships unflagged in Chrome 144, Firefox 139, and Node.js 26. Here's how to actually migrate common Date patterns to Temporal.PlainDate, ZonedDateTime, and Duration, with real code.

Abhishek Gupta

Abhishek Gupta

6 min read

Migrating From Date to Temporal: A Practical JavaScript Guide

Sponsored

Share

JavaScript’s Date object has been wrong in the same handful of ways for thirty years: it’s mutable, it conflates calendar dates with specific instants, months are zero-indexed for no defensible reason, and time zone handling barely exists. Temporal fixes all of it, and as of this year it’s no longer a proposal you have to explain to your team. It reached TC39 Stage 4 in March 2026 and now ships unflagged in Chrome 144, Firefox 139, Edge 144, and Node.js 26. If your codebase has a utils/date-helpers.js file full of defensive workarounds, this is the year to start replacing it.

Why this isn’t just a new Date constructor

The core design decision in Temporal is splitting “a date” into several distinct, immutable types, based on what information you actually have:

TypeRepresentsExample use case
Temporal.PlainDateA calendar date, no time, no zoneA birthday, a deadline, a holiday
Temporal.PlainTimeA wall-clock time, no date, no zone”Store opens at 9:00 AM”
Temporal.PlainDateTimeA date and time, no zoneA form field for “meeting time” before you know the zone
Temporal.ZonedDateTimeA specific instant, tied to a time zone and calendarAn actual scheduled meeting, a log timestamp shown to a user
Temporal.InstantA specific point on the UTC timeline, no calendarRaw timestamps for storage or comparison
Temporal.DurationAn elapsed amount of time”3 days and 2 hours”

Date tried to be all six of these at once, which is why so much Date-related code has comments like // careful, this breaks near DST. Temporal makes you be explicit about which one you have, and the type system (or at minimum, the object you’re holding) won’t let you accidentally do zone-aware arithmetic on a value that has no zone.

Common Date patterns, migrated

Getting “today” as a date, not a timestamp

// Date: you get a full instant, then have to extract just the date parts
const today = new Date();
const dateOnly = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;

// Temporal: PlainDate is already just a date
const today = Temporal.Now.plainDateISO();
today.toString(); // "2026-07-16", no formatting gymnastics

Adding time, correctly, across DST

// Date: this can land on the wrong wall-clock hour across a DST transition
const meeting = new Date("2026-03-08T09:00:00");
meeting.setDate(meeting.getDate() + 1); // silently drifts an hour in some zones

// Temporal: ZonedDateTime.add() handles the calendar and the zone correctly
const meeting = Temporal.ZonedDateTime.from("2026-03-08T09:00:00-05:00[America/New_York]");
const nextDay = meeting.add({ days: 1 });
nextDay.toString(); // still 09:00 local time, DST-adjusted correctly

Comparing dates without timestamp footguns

// Date: comparing requires converting to timestamps, and time-of-day can throw off "same day" checks
const a = new Date("2026-07-16T23:00:00");
const b = new Date("2026-07-17T01:00:00");
a.getTime() < b.getTime(); // true, but are these "the same day"? Depends on zone, easy to get wrong.

// Temporal: PlainDate strips time entirely, so the comparison means what it says
const a = Temporal.PlainDate.from("2026-07-16");
const b = Temporal.PlainDate.from("2026-07-17");
Temporal.PlainDate.compare(a, b); // -1, unambiguous

Formatting for display

Temporal integrates directly with Intl, so you don’t need a separate formatting library for locale-aware output:

const date = Temporal.PlainDate.from("2026-07-16");
new Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(date);
// "July 16, 2026"

new Intl.DateTimeFormat("de-DE", { dateStyle: "long" }).format(date);
// "16. Juli 2026"

Calculating a duration between two events

// Date: manual millisecond math, then manual conversion back to human units
const start = new Date("2026-07-01T09:00:00Z");
const end = new Date("2026-07-16T14:30:00Z");
const diffMs = end - start;
const days = Math.floor(diffMs / 86400000); // and you still don't have hours cleanly

// Temporal: .since() returns a Duration with the units you ask for
const start = Temporal.Instant.from("2026-07-01T09:00:00Z");
const end = Temporal.Instant.from("2026-07-16T14:30:00Z");
const elapsed = end.since(start, { largestUnit: "days" });
elapsed.toString(); // "P15DT5H30M" - 15 days, 5 hours, 30 minutes

Browser and runtime support, as of July 2026

EnvironmentStatus
Chrome / Edge 144+Shipped unflagged
Firefox 139+Shipped unflagged
SafariNot shipped, no Technology Preview yet
Node.js 26+Shipped unflagged
Node.js 24Behind --harmony-temporal flag

Overall browser coverage sits around 69%, and the entire gap is Safari. That means any code reaching real users still needs a polyfill today. @js-temporal/polyfill implements the full spec at about 56 KB minified and gzipped; temporal-polyfill from the FullCalendar team covers the commonly used surface at roughly 20 KB. A feature check keeps the cost near zero for users who don’t need it:

if (!globalThis.Temporal) {
  await import("temporal-polyfill/global");
}

Server-side code on Node.js 26+ needs nothing extra.

A migration strategy that doesn’t require a big-bang rewrite

Temporal and Date can coexist in the same codebase, and Temporal.Instant.fromEpochMilliseconds(date.getTime()) converts one to the other cleanly in either direction. That means you don’t need to migrate everything before shipping anything:

  1. Start with the code that has caused actual bugs. DST-adjacent scheduling logic, anything doing date arithmetic across month or year boundaries, and multi-timezone display code are the highest-value targets.
  2. New code gets Temporal by default. Stop adding new Date usage even before the old code is converted.
  3. Convert at API boundaries. If you store timestamps as ISO strings or epoch milliseconds in a database, Temporal.Instant.from() and .toString() round-trip cleanly, so storage format doesn’t need to change.
  4. Leave display formatting for last. The Intl.DateTimeFormat integration means formatting code is usually the easiest part to port, so it’s a fine place to defer.

If your team is also working through a broader modern web app stack decision this year, Temporal is worth listing as a dependency to drop rather than add: it removes date-fns or Luxon from a meaningful chunk of codebases that only pulled them in to work around exactly the problems Temporal now solves natively.

Thirty years of new Date(year, month - 1, day) footguns are optional now. Start with the file that has the most DST-related bug comments in it, and go from there.

Frequently asked questions

Do I need a library like date-fns or Luxon anymore?
For a lot of what those libraries exist to fix, no. Temporal has built-in arithmetic (add, subtract, until, since), comparison, formatting via Intl, and time zone conversion without a library. You may still reach for date-fns or Luxon for specific formatting helpers or business-calendar logic Temporal doesn't cover, but the core problem those libraries solved, that Date is unsafe to use directly, mostly goes away.
Is Temporal a drop-in replacement for Date?
No, and that's intentional. Date conflates a calendar date, a wall-clock time, and a specific instant into one mutable object, which is the root cause of most date bugs. Temporal makes you pick the right type for what you actually have: Temporal.PlainDate for a calendar date with no time or zone, Temporal.ZonedDateTime for a real moment tied to a time zone, and so on. Migrating means deciding which type each piece of your code actually needs, not swapping one constructor for another.
What happens in Safari, which hasn't shipped Temporal yet?
You need a polyfill for any code that runs in the browser. @js-temporal/polyfill from the TC39 champions implements the full spec at around 56 KB minified and gzipped. temporal-polyfill from FullCalendar covers the commonly used parts at roughly 20 KB. A simple `if (!globalThis.Temporal)` check lets you skip loading the polyfill entirely for users on Chrome, Firefox, or Edge, so most of your traffic pays nothing.
Can I use Temporal in Node.js today without a polyfill?
Yes, if you're on Node.js 26 or later, where Temporal ships unflagged. Node.js 24 supported it behind the --harmony-temporal flag. If your production environment is pinned to an older LTS, use the polyfill there too; the API surface is identical so your code doesn't change when you eventually upgrade the runtime.
What's the single most common Date bug that Temporal actually fixes?
Date math across daylight saving time transitions. Adding 24 hours to a Date object near a DST boundary can land you on the wrong wall-clock time, because Date silently does instant-based arithmetic even when you meant calendar arithmetic. Temporal.ZonedDateTime.add() is explicit about which kind of arithmetic you're doing and handles the DST transition correctly by default.

Sources

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored