Skip to content
100% in your browser. Nothing you paste is uploaded — all processing runs locally. Read more →

Temporal has landed in Chrome 144 — what actually changes for production JS

10 min read #javascript #temporal #date #chrome

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:

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

BrowserStatus
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