The Temporal API is the new JavaScript standard designed to replace the Date object. It offers more rigorous date and time management through dedicated classes, default immutability, native time zone support, and non-Gregorian calendar support. Natively available in Chrome and Firefox, it represents the future of time manipulation in JavaScript.
In this complete guide, we will explore every aspect of the Temporal API: its fundamental classes, their concrete use cases, date arithmetic, duration management, and best practices for integrating it into your projects right now.
Why the Date object is problematic
Before diving into Temporal, it's essential to understand why the current Date object is no longer sufficient. It's not simply a matter of preference: it's a fundamental design problem that causes bugs in virtually every application that manipulates dates.
A poorly designed Java legacy
JavaScript's Date object is directly inspired by Java's java.util.Date class, which was itself considered so problematic that it was replaced in the early 2010s. But JavaScript, true to its backward compatibility principle, kept this object as-is.
Here are some of the most common pitfalls:
// Months start at 0: here we get February 1st, not January
const date = new Date(2025, 1, 1);
console.log(date.toISOString()); // 2025-02-01T00:00:00.000Z
// Invalid dates are silently corrected
const invalid = new Date(2025, 1, 30); // February 30th doesn't exist
console.log(invalid.toISOString()); // 2025-03-02T00:00:00.000Z (March 2nd!)
// getYear() returns years since 1900, not the actual year
const now = new Date();
console.log(now.getYear()); // 125 (in 2025)
console.log(now.getFullYear()); // 2025
The lack of explicit time zones
Internally, a Date object only stores a timestamp in milliseconds since January 1, 1970 UTC. When you display this date, the browser automatically applies the device's local time zone. You cannot create a date in an arbitrary time zone like Asia/Tokyo or America/New_York without resorting to workarounds with Intl.DateTimeFormat.
This behavior is the source of countless bugs, especially in international applications where the same event must be displayed correctly based on each user's time zone.
Problematic mutability
All Date setters modify the object in place, causing side effects that are difficult to track:
const date = new Date(2025, 0, 15);
const copy = date; // This is NOT a copy, it's a reference
copy.setMonth(5);
console.log(date.getMonth()); // 5 — the original date is modified!
Introduction to the Temporal API
Temporal is a global namespace, similar to Math or Intl. It is not a constructor: you cannot write new Temporal(). All its properties and methods are static, and it exposes over 200 utility methods spread across several specialized classes.
Temporal's core philosophy rests on one principle: one type per use case. Instead of a single object that tries to do everything (like Date), Temporal offers distinct classes, each tailored to a specific need.
Main Temporal classes
| Class | Description | Example usage |
|---|---|---|
Temporal.PlainDate | Date without time or time zone | Birthday, public holiday |
Temporal.PlainTime | Time without date or time zone | Daily alarm, opening hours |
Temporal.PlainDateTime | Date + time, without time zone | Local appointment |
Temporal.ZonedDateTime | Date + time + time zone | International event, flight |
Temporal.Instant | Precise point on the timeline (timestamp) | Server log, technical timestamp |
Temporal.Duration | Duration between two points | Time remaining, trip duration |
Temporal.PlainYearMonth | Year + month only | Monthly billing |
Temporal.PlainMonthDay | Month + day only | Recurring anniversary |
Browser support and polyfills
As of mid-2025, Temporal is natively supported by Chrome and Firefox. Safari is in the process of implementing it. For Node.js, you can enable it via the --harmony-temporal flag. Bun is also working on its integration.
For immediate production use, two polyfills are recommended:
# Official polyfill by proposal champions
npm install @js-temporal/polyfill
# Alternative polyfill by FullCalendar
npm install @fullcalendar/temporal-polyfill// Using the polyfill
import { Temporal } from '@js-temporal/polyfill';
const today = Temporal.Now.plainDateISO();
console.log(today.toString()); // 2025-07-11
Creating Temporal objects
The Temporal API provides three consistent creation methods, available on each class. This uniformity is one of its greatest advantages over Date.
Via Temporal.Now
The Temporal.Now namespace provides methods to get the current instant in various formats:
// Precise instant (timestamp with UTC time zone)
const instant = Temporal.Now.instant();
console.log(instant.toString()); // 2025-07-11T14:30:00.123456789Z
// Local date and time (without time zone)
const dateTime = Temporal.Now.plainDateTimeISO();
console.log(dateTime.toString()); // 2025-07-11T16:30:00.123456789
// Today's date only
const today = Temporal.Now.plainDateISO();
console.log(today.toString()); // 2025-07-11
// Current time only
const time = Temporal.Now.plainTimeISO();
console.log(time.toString()); // 16:30:00.123456789
// Date and time with time zone
const zoned = Temporal.Now.zonedDateTimeISO();
console.log(zoned.toString()); // 2025-07-11T16:30:00.123456789+02:00[Europe/Paris]
// System time zone identifier
const tz = Temporal.Now.timeZoneId();
console.log(tz); // Europe/Paris
Via the constructor
Each class exposes a constructor that accepts numeric values. Unlike Date, months are indexed starting from 1:
// PlainDate: year, month, day
const date = new Temporal.PlainDate(2025, 1, 1);
console.log(date.toString()); // 2025-01-01 (indeed January 1st)
// PlainDateTime: year, month, day, hour, minute, second
const dateTime = new Temporal.PlainDateTime(2025, 12, 25, 18, 30, 0);
console.log(dateTime.toString()); // 2025-12-25T18:30:00
// PlainTime: hour, minute, second
const time = new Temporal.PlainTime(8, 0, 0);
console.log(time.toString()); // 08:00:00Unlike Date which silently corrects invalid values, Temporal throws a RangeError if you pass impossible values like February 30th.
Via the from() method
The static from() method is the most versatile. It accepts an ISO 8601 string or a properties object:
// From an ISO string
const date = Temporal.PlainDate.from('2025-08-15');
const dateTime = Temporal.PlainDateTime.from('2025-08-15T09:00:00');
// From a properties object
const dateObj = Temporal.PlainDate.from({ year: 2025, month: 8, day: 15 });
// ZonedDateTime with full time zone
const zoned = Temporal.ZonedDateTime.from(
'2025-08-15T09:00:00+02:00[Europe/Paris]'
);
console.log(zoned.toString());
// 2025-08-15T09:00:00+02:00[Europe/Paris]
// ZonedDateTime from an object
const zonedObj = Temporal.ZonedDateTime.from({
timeZone: 'America/New_York',
year: 2025,
month: 8,
day: 15,
hour: 9,
minute: 0
});
console.log(zonedObj.toString());
// 2025-08-15T09:00:00-04:00[America/New_York]
Reading properties of a Temporal object
Each Temporal class exposes read-only properties that allow you to extract precisely the information you need. No more getMonth() and getFullYear(): you access properties directly.
const date = Temporal.PlainDate.from('2025-07-14');
console.log(date.year); // 2025
console.log(date.month); // 7
console.log(date.day); // 14
console.log(date.dayOfWeek); // 1 (Monday)
console.log(date.dayOfYear); // 195
console.log(date.daysInMonth); // 31
console.log(date.daysInYear); // 365
console.log(date.monthsInYear);// 12
console.log(date.inLeapYear); // false
console.log(date.weekOfYear); // 29
console.log(date.yearOfWeek); // 2025For a ZonedDateTime, you get additional properties related to the time zone:
const zdt = Temporal.ZonedDateTime.from(
'2025-07-14T15:30:00+02:00[Europe/Paris]'
);
console.log(zdt.timeZoneId); // Europe/Paris
console.log(zdt.offset); // +02:00
console.log(zdt.hoursInDay); // 24
console.log(zdt.epochMilliseconds); // timestamp in milliseconds
console.log(zdt.epochNanoseconds); // timestamp in nanoseconds (BigInt)
Manipulating and modifying dates
One of Temporal's fundamental characteristics is immutability. Every modification operation returns a new object, leaving the original intact. This eliminates an entire category of bugs related to side effects.
Modifying with with()
The with() method creates a copy of the object, replacing only the specified fields:
const date = Temporal.PlainDateTime.from('2025-01-01T12:00:00');
// Change only the year
const date2030 = date.with({ year: 2030 });
console.log(date2030.toString()); // 2030-01-01T12:00:00
// The original is preserved
console.log(date.toString()); // 2025-01-01T12:00:00
// Change multiple fields at once
const modified = date.with({ month: 6, day: 15, hour: 18 });
console.log(modified.toString()); // 2025-06-15T18:00:00
Changing time zones with withTimeZone()
For ZonedDateTime objects, you can convert to another time zone. The absolute instant remains the same, only the local representation changes:
const paris = Temporal.ZonedDateTime.from(
'2025-07-14T12:00:00+02:00[Europe/Paris]'
);
const tokyo = paris.withTimeZone('Asia/Tokyo');
console.log(tokyo.toString());
// 2025-07-14T19:00:00+09:00[Asia/Tokyo]
const newYork = paris.withTimeZone('America/New_York');
console.log(newYork.toString());
// 2025-07-14T06:00:00-04:00[America/New_York]
Additional ZonedDateTime methods
ZonedDateTime also provides withCalendar() to change the calendar system and withPlainTime() to replace the time portion:
const zdt = Temporal.ZonedDateTime.from(
'2025-07-14T15:30:00+02:00[Europe/Paris]'
);
// Go back to midnight
const midnight = zdt.startOfDay();
console.log(midnight.toString());
// 2025-07-14T00:00:00+02:00[Europe/Paris]
// Replace the time with a specific value
const at9am = zdt.withPlainTime('09:00:00');
console.log(at9am.toString());
// 2025-07-14T09:00:00+02:00[Europe/Paris]
Date arithmetic
Time arithmetic is one of the areas where Temporal most clearly outperforms the Date object. The add() and subtract() methods accept an object describing the duration to add or subtract.
Adding and subtracting time
const date = Temporal.PlainDate.from('2025-01-01');
// Add 4 days
const plus4 = date.add({ days: 4 });
console.log(plus4.toString()); // 2025-01-05
// Add 40 days (automatically handles month changes)
const plus40 = date.add({ days: 40 });
console.log(plus40.toString()); // 2025-02-10
// Subtract 2 months and 15 days
const minus = date.subtract({ months: 2, days: 15 });
console.log(minus.toString()); // 2024-10-17
// Add a complex duration
const future = date.add({ years: 1, months: 6, days: 10 });
console.log(future.toString()); // 2026-07-11
Arithmetic with DST handling
When using ZonedDateTime, arithmetic automatically accounts for daylight saving time (DST) changes. This is a considerable advantage over Date:
// DST change in France occurs on the last Sunday of March
const beforeDST = Temporal.ZonedDateTime.from(
'2025-03-29T12:00:00+01:00[Europe/Paris]'
);
const afterDST = beforeDST.add({ days: 1 });
console.log(afterDST.toString());
// 2025-03-30T12:00:00+02:00[Europe/Paris]
// The offset changed from +01:00 to +02:00 automatically
Calculating the difference between two dates
The until() and since() methods return a Temporal.Duration object representing the difference between two dates:
const start = Temporal.PlainDate.from('2025-01-01');
const end = Temporal.PlainDate.from('2025-07-21');
// By default, duration is in days
const duration = start.until(end);
console.log(duration.toString()); // P201D (201 days)
// To get months and days, specify largestUnit
const detailed = start.until(end, { largestUnit: 'months' });
console.log(detailed.toString()); // P6M20D (6 months and 20 days)
// since() is the inverse of until()
const since = end.since(start, { largestUnit: 'months' });
console.log(since.toString()); // P6M20D
Comparing two dates
Comparison is done via the static compare() methods and the instance equals() method:
const date1 = Temporal.PlainDate.from('2025-01-01');
const date2 = Temporal.PlainDate.from('2025-07-14');
// compare() returns -1, 0, or 1
console.log(Temporal.PlainDate.compare(date1, date2)); // -1 (date1 before date2)
console.log(Temporal.PlainDate.compare(date2, date1)); // 1
console.log(Temporal.PlainDate.compare(date1, date1)); // 0
// equals() to test equality
console.log(date1.equals(Temporal.PlainDate.from('2025-01-01'))); // true
// Sorting an array of dates
const dates = [
Temporal.PlainDate.from('2025-12-25'),
Temporal.PlainDate.from('2025-01-01'),
Temporal.PlainDate.from('2025-07-14')
];
dates.sort(Temporal.PlainDate.compare);
console.log(dates.map(d => d.toString()));
// ['2025-01-01', '2025-07-14', '2025-12-25']Never use === to compare two Temporal objects. Like any JavaScript object, this compares memory references, not values. Always use equals() or compare().
Managing durations with Temporal.Duration
Temporal.Duration represents a duration explicitly. Unlike the millisecond calculations that Date requires, you work directly with readable units.
Creating and manipulating durations
// Create a duration from an object
const duration = Temporal.Duration.from({
hours: 130,
minutes: 20
});
// Get the total in seconds
const totalSeconds = duration.total({ unit: 'second' });
console.log(totalSeconds); // 469200
// Create a duration from an ISO 8601 string
const d2 = Temporal.Duration.from('P1Y6M15DT12H30M');
console.log(d2.years); // 1
console.log(d2.months); // 6
console.log(d2.days); // 15
console.log(d2.hours); // 12
console.log(d2.minutes); // 30
Rounding a duration
The round() method allows you to round a duration. Note: conversions involving months or years require a relativeTo, because a month doesn't always have the same number of days:
const duration = Temporal.Duration.from({ months: 6, days: 20 });
// Round to days (requires relativeTo for months)
const inDays = duration.round({
largestUnit: 'days',
relativeTo: Temporal.PlainDate.from('2025-01-01')
});
console.log(inDays.toString()); // P201D
// For sub-day units, no relativeTo needed
const hours = Temporal.Duration.from({ minutes: 150 });
const rounded = hours.round({ largestUnit: 'hours' });
console.log(rounded.toString()); // PT2H30M
Formatting a duration with Intl.DurationFormat
The Intl.DurationFormat API allows you to display durations in a localized manner:
const duration = Temporal.Duration.from({ months: 6, days: 20, hours: 3 });
const formatter = new Intl.DurationFormat('en', { style: 'long' });
console.log(formatter.format(duration));
// "6 months, 20 days, and 3 hours"
const shortFormatter = new Intl.DurationFormat('en', { style: 'short' });
console.log(shortFormatter.format(duration));
// "6 mo, 20 days, 3 hr"
Conversions between Temporal classes
A major advantage of the Temporal API is the ease with which you can convert one type to another. Each class exposes methods like toPlainDate(), toPlainTime(), toZonedDateTime(), etc.
// From ZonedDateTime to "Plain" types
const zdt = Temporal.ZonedDateTime.from(
'2025-07-14T15:30:00+02:00[Europe/Paris]'
);
const plainDate = zdt.toPlainDate();
console.log(plainDate.toString()); // 2025-07-14
const plainTime = zdt.toPlainTime();
console.log(plainTime.toString()); // 15:30:00
const plainDateTime = zdt.toPlainDateTime();
console.log(plainDateTime.toString()); // 2025-07-14T15:30:00
const instant = zdt.toInstant();
console.log(instant.toString()); // 2025-07-14T13:30:00Z
// From PlainDate to ZonedDateTime (requires time zone and time)
const date = Temporal.PlainDate.from('2025-07-14');
const zoned = date.toZonedDateTime({
timeZone: 'Europe/Paris',
plainTime: Temporal.PlainTime.from('09:00:00')
});
console.log(zoned.toString());
// 2025-07-14T09:00:00+02:00[Europe/Paris]
// From PlainDate to PlainYearMonth or PlainMonthDay
const ym = date.toPlainYearMonth();
console.log(ym.toString()); // 2025-07
const md = date.toPlainMonthDay();
console.log(md.toString()); // 07-14
Non-Gregorian calendar support
Temporal natively supports multiple calendar systems, which is essential for international applications. By default, all objects use the ISO 8601 calendar (based on the Gregorian), but you can work with other calendars.
// Convert a Gregorian date to the Hebrew calendar
const isoDate = Temporal.PlainDate.from('2025-07-14');
const hebrewDate = isoDate.withCalendar('hebrew');
console.log(hebrewDate.toString()); // date in Hebrew format
console.log(hebrewDate.year); // year in the Hebrew calendar
console.log(hebrewDate.month); // month in the Hebrew calendar
// Japanese calendar (imperial eras)
const jpDate = isoDate.withCalendar('japanese');
console.log(jpDate.era); // "reiwa"
console.log(jpDate.eraYear); // 7 (Reiwa 7)
// Localized display with Intl
console.log(hebrewDate.toLocaleString('en-US', {
calendar: 'hebrew',
dateStyle: 'full'
}));The list of supported calendars can be obtained via Intl.supportedValuesOf('calendar'). Among the most common: islamic, hebrew, chinese, japanese, persian, buddhist.
Integration with HTML inputs
The Temporal API integrates naturally with HTML <input> elements of type date, time, and datetime-local. These fields return values in ISO format that from() accepts directly.
// Input of type "date"
const dateInput = document.querySelector('#date-input');
dateInput.addEventListener('change', () => {
const date = Temporal.PlainDate.from(dateInput.value);
console.log(date.toString());
console.log(date.dayOfWeek); // Day of the week
});
// Input of type "time"
const timeInput = document.querySelector('#time-input');
timeInput.addEventListener('change', () => {
const time = Temporal.PlainTime.from(timeInput.value);
const in3Hours = time.add({ hours: 3 });
console.log(in3Hours.toString()); // e.g.: "18:24:00"
});
// Input of type "datetime-local" converted to ZonedDateTime
const dtInput = document.querySelector('#datetime-input');
dtInput.addEventListener('change', () => {
const plain = Temporal.PlainDateTime.from(dtInput.value);
const zoned = plain.toZonedDateTime('Europe/Paris');
console.log(zoned.toString());
});
The RFC 9557 format
All Temporal classes use the RFC 9557 format for serialization and deserialization. This format is an extension of ISO 8601 / RFC 3339 and adds the ability to specify the time zone and calendar system:
YYYY-MM-DD T HH:mm:ss.sssssssss Z/±HH:mm [time_zone_id] [u-ca=calendar_id]Some concrete examples:
// Instant (always in UTC)
'2025-07-14T13:30:00Z'
// ZonedDateTime with time zone and calendar
'2025-07-14T15:30:00+02:00[Europe/Paris][u-ca=iso8601]'
// Simple PlainDate
'2025-07-14'
// Simple PlainTime
'15:30:00'
// PlainDateTime
'2025-07-14T15:30:00'This format allows precision up to the nanosecond (9 decimal places), compared to milliseconds for Date. This is particularly useful for scientific applications or systems requiring high temporal precision.
Best practices for using Temporal
Here are the essential recommendations to get the most out of the Temporal API in your projects:
1. Choose the right type for each use case. Don't use ZonedDateTime everywhere by default. A birthday is a PlainDate, a daily alarm is a PlainTime, an international event is a ZonedDateTime.
2. Don't mix year and era/eraYear. If you're using calendars with eras, choose a single representation and stay consistent.
3. Always use compare() or equals() for comparisons. The === operator compares references, not values.
4. Specify relativeTo for duration conversions involving months or years. A month doesn't always have the same number of days.
5. Never assume a week is 7 days. Use daysInWeek for robust code with non-Gregorian calendars.
6. Prefer monthCode over month for storing recurring data (like birthdays), because monthCode corresponds to the month's name regardless of leap months.
7. Use a polyfill in production until support is universal, and test on all your target environments.
Migration from Date to Temporal
Here's a summary of equivalences between the old Date and the new Temporal API:
| Operation | With Date | With Temporal |
|---|---|---|
| Current date | new Date() | Temporal.Now.plainDateISO() |
| Current timestamp | Date.now() | Temporal.Now.instant() |
| Create a date | new Date(2025, 0, 1) | new Temporal.PlainDate(2025, 1, 1) |
| Parse a string | new Date('2025-01-01') | Temporal.PlainDate.from('2025-01-01') |
| Add days | date.setDate(date.getDate() + 7) | date.add({ days: 7 }) |
| Difference | (date2 - date1) / 86400000 | date1.until(date2) |
| Format | date.toLocaleDateString() | date.toLocaleString() |
| Compare | date1.getTime() === date2.getTime() | date1.equals(date2) |
Frequently asked questions
Is the Temporal API ready for production?
Temporal has been natively supported by Chrome and Firefox since 2025. For other browsers and environments, mature polyfills like @js-temporal/polyfill allow you to use it right now. For new projects, it's a wise choice. For existing projects, a progressive migration is recommended.
Does Temporal replace libraries like Moment.js or date-fns?
Yes, in the vast majority of cases. Temporal natively covers the main use cases of these libraries: parsing, formatting, arithmetic, time zones, and calendars. The advantage is that being built into the language, it requires no external dependencies and will always be maintained with the JavaScript engine.
What is the difference between Temporal.Instant and Temporal.ZonedDateTime?
Temporal.Instant represents an absolute point on the timeline (a timestamp in nanoseconds since the Unix epoch), with no notion of time zone or calendar. Temporal.ZonedDateTime combines an instant with a time zone and a calendar, which allows obtaining a human-readable date and time in a given region.
How does Temporal handle daylight saving time (DST) changes?
ZonedDateTime automatically handles DST transitions. When you perform arithmetic operations, the offset is adjusted. For ambiguous moments (during the fall-back when an hour exists twice), Temporal provides ambiguity resolution options via the disambiguation parameter in conversion methods.





