Skip to content

Web Development · JavaScript

The JavaScript Temporal API: Finally a Date Object That Works

The Date object has been broken for 30 years. Temporal is its replacement, now shipping in browsers and Node.js. Here's what actually changed and how to use it.

Anurag Verma

Anurag Verma

7 min read

The JavaScript Temporal API: Finally a Date Object That Works

Sponsored

Share

Every JavaScript developer has a war story about dates. You subtract two timestamps and get a wrong result because one was in local time and the other wasn’t. You use new Date() in a Node.js cron job and the output changes depending on what server timezone the container happens to be in. You pass a date string to Date.parse() and it works in Chrome but silently returns NaN in Safari.

The Temporal API is the fix for all of that. After years of development in TC39, it has shipped as stable in major JavaScript engines and is available without a polyfill in Node.js 22+ and modern browsers. This is the API that should have existed from the beginning.

What Was Wrong with Date

Date has three fundamental problems that Temporal addresses:

1. No timezone-aware types. Date represents a single instant in time. It has no concept of a date in a timezone, a date without time, or a time without a date. When you call date.getHours(), you get the hours in the local system timezone. When you call date.getUTCHours(), you get UTC. There is no way to express “3pm in Berlin” as a Date without immediately resolving it to milliseconds since epoch and losing the timezone information.

2. Mutability. Every Date method that modifies a date (like setMonth) mutates the object in place. This makes dates passed around in code unpredictable. You pass a date into a function, the function calls setMonth on it for some calculation, and now the caller’s date is wrong.

3. Inconsistent parsing. Date.parse and new Date(string) behavior varies across implementations. The string "2024-02-30" returns NaN in some engines and a rollover date in others. ISO 8601 strings are parsed as UTC in newer environments but as local time in some older ones.

Libraries like Luxon and date-fns have been the workaround. Temporal replaces the need for them.

The Temporal Types

Temporal is not a single class. It’s a collection of distinct types, each representing a specific concept:

TypeWhat It Represents
Temporal.InstantAn exact point in time (like a Unix timestamp)
Temporal.ZonedDateTimeA date and time in a specific timezone
Temporal.PlainDateTimeA date and time without timezone
Temporal.PlainDateA calendar date (no time, no timezone)
Temporal.PlainTimeA wall-clock time (no date, no timezone)
Temporal.PlainYearMonthA month in a year
Temporal.PlainMonthDayA recurring date (like a birthday)
Temporal.DurationAn amount of time

Choosing the right type prevents entire categories of bugs. If your function only cares about a calendar date (was the invoice issued before a deadline?), use PlainDate. You can’t accidentally compare it to a time.

Getting the Current Time

// An exact moment in time (equivalent to Date.now())
const now = Temporal.Now.instant();
console.log(now.epochMilliseconds); // 1748822400000

// The current date and time in a specific timezone
const nyTime = Temporal.Now.zonedDateTimeISO('America/New_York');
console.log(nyTime.toString());
// "2026-06-02T10:30:00-04:00[America/New_York]"

// Just the calendar date in the system's local timezone
const today = Temporal.Now.plainDateISO();
console.log(today.toString()); // "2026-06-02"

The timezone is explicit. If you want New York time, you ask for it. There is no implicit system timezone leaking into your calculations.

Creating Dates and Times

// Create a specific date
const released = Temporal.PlainDate.from('2026-01-15');
const alsoReleased = Temporal.PlainDate.from({ year: 2026, month: 1, day: 15 });

// Create a zoned datetime
const meeting = Temporal.ZonedDateTime.from({
  year: 2026,
  month: 6,
  day: 15,
  hour: 14,
  minute: 30,
  timeZone: 'Europe/Berlin',
});

// Convert between timezones
const meetingInTokyo = meeting.withTimeZone('Asia/Tokyo');
console.log(meetingInTokyo.toString());
// "2026-06-15T21:30:00+09:00[Asia/Tokyo]"

The objects are immutable. withTimeZone returns a new object; it does not modify meeting.

Arithmetic

Date math with Date requires manual timestamp manipulation. Temporal makes it readable:

const start = Temporal.PlainDate.from('2026-01-01');

// Add time
const threeMonthsLater = start.add({ months: 3 });
console.log(threeMonthsLater.toString()); // "2026-04-01"

// Subtract time
const yesterday = Temporal.Now.plainDateISO().subtract({ days: 1 });

// Calculate a duration between two dates
const end = Temporal.PlainDate.from('2026-12-31');
const duration = start.until(end);
console.log(duration.toString()); // "P364D"
console.log(duration.days);       // 364

// Duration with months and days
const fullDuration = start.until(end, { largestUnit: 'month' });
console.log(`${fullDuration.months} months, ${fullDuration.days} days`);
// "11 months, 30 days"

Month-aware arithmetic handles edge cases correctly. Adding one month to January 31 gives February 28 (or 29), not March 2. This is the behavior you want 99% of the time and the behavior that trips up manual timestamp math.

Comparison and Sorting

const a = Temporal.PlainDate.from('2026-03-15');
const b = Temporal.PlainDate.from('2026-06-01');

// Compare
console.log(Temporal.PlainDate.compare(a, b)); // -1 (a is before b)
console.log(a.equals(b)); // false

// Sort an array of dates
const dates = [
  Temporal.PlainDate.from('2026-12-01'),
  Temporal.PlainDate.from('2026-01-15'),
  Temporal.PlainDate.from('2026-06-30'),
];
dates.sort(Temporal.PlainDate.compare);
// [2026-01-15, 2026-06-30, 2026-12-01]

No more a.getTime() - b.getTime() for sorting.

Timezone-Safe Business Logic

Here’s where Temporal really pays off. A common real-world requirement: “is this timestamp within business hours in the customer’s timezone?”

function isWithinBusinessHours(instant, customerTimezone) {
  const localTime = instant.toZonedDateTimeISO(customerTimezone);
  const hour = localTime.hour;
  const dayOfWeek = localTime.dayOfWeek; // 1 = Monday, 7 = Sunday

  return dayOfWeek >= 1 && dayOfWeek <= 5 && hour >= 9 && hour < 17;
}

const requestTime = Temporal.Instant.from('2026-06-02T14:30:00Z');
console.log(isWithinBusinessHours(requestTime, 'America/Chicago')); // true (9:30am CT)
console.log(isWithinBusinessHours(requestTime, 'Asia/Kolkata'));    // false (8:00pm IST)

With Date, this requires manually computing the UTC offset for each timezone, accounting for DST, and hoping the logic is right. With Temporal, the timezone is part of the type.

Handling DST Transitions

Daylight saving time transitions are where Date arithmetic silently produces wrong results. Temporal is explicit about ambiguity:

// Clocks in New York jump from 2:00am to 3:00am on March 8, 2026.
// "2:30am" doesn't exist that day.
const ambiguous = Temporal.ZonedDateTime.from(
  '2026-03-08T02:30:00[America/New_York]',
  { disambiguation: 'earlier' } // or 'later', 'compatible', 'reject'
);

With 'reject', Temporal throws a RangeError. With 'earlier' or 'later', it picks the time before or after the gap. You choose explicitly, rather than getting a silent wrong answer.

Using Temporal in TypeScript

Temporal works cleanly with TypeScript. The types are available via @js-temporal/polyfill if you’re on an older runtime, or natively on Node.js 22+:

function formatDeadline(deadline: Temporal.PlainDate): string {
  const today = Temporal.Now.plainDateISO();
  const daysLeft = today.until(deadline).days;

  if (daysLeft < 0) {
    return `Overdue by ${Math.abs(daysLeft)} days`;
  } else if (daysLeft === 0) {
    return 'Due today';
  } else {
    return `${daysLeft} days remaining`;
  }
}

const projectDeadline = Temporal.PlainDate.from('2026-06-30');
console.log(formatDeadline(projectDeadline)); // "28 days remaining"

The static from and compare methods, combined with TypeScript’s type checking, catch mixing of incompatible date types at compile time. You can’t accidentally pass a PlainDateTime where a PlainDate is expected.

Migrating from Date

You won’t need to rewrite everything at once. Temporal interoperates with Date:

// Convert from Date to Temporal
const legacyDate = new Date();
const instant = Temporal.Instant.fromEpochMilliseconds(legacyDate.getTime());
const zonedDT = instant.toZonedDateTimeISO('UTC');

// Convert from Temporal to Date (loses timezone info)
const backToDate = new Date(instant.epochMilliseconds);

For new code, use Temporal directly. For existing code, migrate when you’re already touching a date-handling function, not as a wholesale refactor.

The Temporal API is not a library you evaluate and decide whether to adopt. It’s a language feature that solves a 30-year-old problem. The question isn’t whether to use it; it’s how quickly you migrate your date handling to it.

Sponsored

Enjoyed it? Pass it on.

Share this article.

Sponsored

The dispatch

Working notes from
the studio.

A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.

No spam, ever. Unsubscribe anytime.

Discussion

Join the conversation.

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

Sponsored