Temporal has landed in Chrome 144 — what actually changes for production JS
JavaScript’s Date object has been a running joke for two decades.
Mutable. Timezone-confused. Months are 0-indexed for some methods
and 1-indexed for others. new Date("2025-01-01") parses as UTC
midnight in some browsers and local midnight in others.
In January 2026, Chrome 144 shipped the Temporal API — the designed replacement that’s been in TC39 standards purgatory since 2017. Firefox 139 followed; Safari has it behind a flag and is expected to ship unflagged in 17.4.
This post walks through the API in production terms: what you actually use, what migration looks like, what breaks, and where Temporal still requires care.
Why Temporal exists
Date was added to JavaScript in 1995, copied from Java’s pre-1.1
date class. Java itself replaced its date system twice before
abandoning that lineage entirely in Java 8. JavaScript was stuck
with the original.
The fundamental problems:
- Mutability.
date.setFullYear(2026)mutates in place. Half the bugs in date code start here. - No native timezone support. A
Dateknows the absolute UTC instant but can’t represent “midnight in Tokyo” except via formatting tricks. - Naive parsing.
Date.parseaccepts anything that vaguely looks like a date and silently produces garbage for ambiguous input. - Months are 0-indexed but days aren’t. Iconic.
- No clean way to compare or do arithmetic. Subtracting two
Dates gives milliseconds; adding “one month” requires manual carry handling.
For two decades, the fix was a third-party library: Moment.js, day.js, Luxon, date-fns. Each had its own API and its own bugs. Adding a 70KB dependency to display a relative timestamp was absurd, and everyone knew it.
Temporal is the language’s own answer.
What you’ll actually use
The Temporal API is namespaced under Temporal. The three classes
that cover 90% of typical usage:
Temporal.Now — get the current time
Temporal.Now.instant(); // exact UTC instant
Temporal.Now.zonedDateTimeISO(); // current local time with timezone
Temporal.Now.plainDateISO(); // today's calendar date in your locale
Three different “now”s for different purposes. No more arguing
about whether Date.now() is wall clock or monotonic (it’s wall);
each form is explicit.
Temporal.PlainDate — a calendar date with no time component
const today = Temporal.PlainDate.from("2026-04-27");
const inTwoWeeks = today.add({ weeks: 2 });
console.log(inTwoWeeks.toString()); // "2026-05-11"
// Comparison
today.compare(inTwoWeeks); // -1 (today < inTwoWeeks)
// Properties
today.year; // 2026
today.month; // 4 (1-indexed!)
today.day; // 27
today.dayOfWeek; // 1 (Monday)
Add/subtract returns a new instance — immutable.
Temporal.ZonedDateTime — a moment in a specific timezone
const meeting = Temporal.ZonedDateTime.from("2026-04-27T14:00[America/New_York]");
console.log(meeting.toString());
// "2026-04-27T14:00:00-04:00[America/New_York]"
// Convert to another timezone
const meetingInTokyo = meeting.withTimeZone("Asia/Tokyo");
console.log(meetingInTokyo.toString());
// "2026-04-28T03:00:00+09:00[Asia/Tokyo]"
// Add a day, respecting DST and timezone
const tomorrow = meeting.add({ days: 1 });
The format includes the timezone in brackets at the end. This is
unambiguous: 2026-04-27T14:00-04:00 could be in any timezone that
happens to be -04:00 right now (EDT in summer, AST year-round in
parts of Canada). [America/New_York] pins it.
Temporal.Duration — a time difference
const trip = Temporal.Duration.from({ days: 3, hours: 4 });
const arrivalTime = departureTime.add(trip);
// Subtract two timestamps
const diff = endTime.since(startTime);
console.log(diff.toString()); // "PT4H35M" (ISO 8601 duration)
console.log(diff.total({ unit: "minutes" })); // 275
Migration patterns
”How long ago was this?”
Old:
function relativeTime(date) {
const ms = Date.now() - date.getTime();
const seconds = Math.floor(ms / 1000);
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
// ... etc
}
New:
function relativeTime(zoned) {
const now = Temporal.Now.zonedDateTimeISO();
const diff = now.since(zoned, { largestUnit: "year" });
// diff has years, months, days, hours, minutes, seconds
if (diff.years > 0) return `${diff.years}y ago`;
if (diff.months > 0) return `${diff.months}mo ago`;
if (diff.days > 0) return `${diff.days}d ago`;
if (diff.hours > 0) return `${diff.hours}h ago`;
if (diff.minutes > 0) return `${diff.minutes}m ago`;
return `${diff.seconds}s ago`;
}
For pure formatting, also see Intl.RelativeTimeFormat — built into
every browser since 2019.
”Format a timestamp for display”
Old:
new Date(timestamp).toLocaleString("en-US", {
year: "numeric", month: "long", day: "numeric",
hour: "2-digit", minute: "2-digit",
timeZone: "America/New_York",
});
New:
Temporal.Instant.fromEpochMilliseconds(timestamp)
.toZonedDateTimeISO("America/New_York")
.toLocaleString("en-US", {
year: "numeric", month: "long", day: "numeric",
hour: "2-digit", minute: "2-digit",
});
The Intl integration carries over. The improvement is that the
intermediate object (ZonedDateTime) is queryable and manipulable.
”Add 30 days, respecting DST”
Old (broken):
const newDate = new Date(oldDate.getTime() + 30 * 24 * 60 * 60 * 1000);
// Wrong if a DST transition happens in those 30 days — you get
// an off-by-one-hour bug
New (correct):
const newDate = oldZonedDateTime.add({ days: 30 });
// Adds 30 *calendar* days, advancing through DST transitions
// without drift
This is the bug that has bitten every scheduling/billing/ calendar app in JavaScript. Temporal fixes it permanently.
”Get the start of the day in a specific timezone”
Old (verbose):
const date = new Date();
date.setHours(0, 0, 0, 0); // start of local day, not Tokyo
// To get start-of-day in Tokyo, you have to do tortured timezone math
New (one line):
Temporal.Now.plainDateISO("Asia/Tokyo").toZonedDateTime("Asia/Tokyo");
Interop with Date
Real codebases will mix Temporal and Date for years. Conversion
in both directions:
// Date → Temporal
const date = new Date();
const instant = date.toTemporalInstant();
// Then specify a timezone:
const zoned = instant.toZonedDateTimeISO("America/New_York");
// Temporal → Date
const date2 = new Date(instant.epochMilliseconds);
Date.prototype.toTemporalInstant() is part of the proposal; it’s
the minimal bridge. Once you have an Instant, convert to whatever
calendar/timezone you need.
Browser support and polyfill
| Browser | Status |
|---|---|
| Chrome 144+ (Jan 2026) | Shipped, unflagged |
| Edge 144+ | Same as Chrome |
| Firefox 139+ | Shipped, unflagged |
| Safari 17.4+ | Behind flag (early 2026); unflagged expected mid-2026 |
| Node.js 22+ | Behind --experimental-temporal-api flag |
| Node.js 24+ | Unflagged |
For broader support, polyfill via @js-temporal/polyfill.
~30KB minified, follows the spec exactly:
import "@js-temporal/polyfill"; // shims globalThis.Temporal
// Now use Temporal as if it were native
The polyfill is what most production code in 2026 will ship until Safari catches up.
Common gotchas
1. month is 1-indexed in Temporal
Unlike Date. This is the right choice but easy to get wrong if
you’re porting code:
new Date(2026, 0, 1); // January 1, 2026 (month = 0)
Temporal.PlainDate.from({ year: 2026, month: 1, day: 1 }); // also January 1
2. from is strict about input
Temporal.PlainDate.from("2026-04-27"); // ✓
Temporal.PlainDate.from("April 27, 2026"); // ✗ throws
Temporal.PlainDate.from("4/27/2026"); // ✗ throws
This is intentional — Temporal refuses to guess. For human-readable
parsing, use Date.parse and convert to Temporal, accepting that
you’ve taken on the ambiguity yourself.
3. Equality and comparison
=== doesn’t work on Temporal objects (they’re objects). Use
.equals() or .compare():
const a = Temporal.PlainDate.from("2026-04-27");
const b = Temporal.PlainDate.from("2026-04-27");
a === b; // false (different objects)
a.equals(b); // true
Temporal.PlainDate.compare(a, b); // 0
4. Don’t store ZonedDateTime in JSON
ZonedDateTime.toString() includes the timezone, which is great
for round-tripping. But many JSON consumers expect just an ISO
8601 timestamp without the [timezone] suffix. For external
APIs, prefer Instant.toString() (just the UTC moment) and pair
it with a separate timezone field if you need both.
When to migrate
For new code: use Temporal directly with the polyfill, since the API is more correct. For existing code: migrate gradually starting with the parts that have bugs (DST handling, multi-timezone display, date arithmetic). Don’t rewrite working code just for the cleanliness.
The single biggest win: any place you currently use moment.js or
day.js, you can drop the dependency. Both libraries’ APIs map
cleanly to Temporal. date-fns users have a slightly bigger
migration but get to delete a lot of imports.
Try the conversion
Paste any timestamp into our epoch converter — it shows the
JavaScript new Date(...) form, which you can plug into the
Temporal interop:
const date = new Date(/* paste here */);
const zoned = date.toTemporalInstant().toZonedDateTimeISO("UTC");
For testing Temporal expressions interactively, use the Chrome DevTools console on Chrome 144+ — the API is globally available without import.
Further reading
- TC39 Temporal proposal
- MDN Temporal documentation
- Cookbook of Temporal recipes
@js-temporal/polyfill- Our reference: What is Unix epoch?, Seconds vs milliseconds
- Related: date.tooljo.com for date math (days-between, countdowns) — uses UTC math under the hood for the same reason Temporal does. date.tooljo.com/date-format-reference is the cross-format quick lookup.