Skip to content
Home
JavaScript Temporal API: Modern Date and Time Handling

JavaScript Temporal API: Modern Date and Time Handling

JavaScript JavaScript 8 min read 1569 words Beginner ExcellentWiki Editorial Team

The JavaScript Temporal API is a comprehensive replacement for the notoriously problematic Date object. After years in development as a TC39 proposal, Temporal reached Stage 3 in 2023 and is shipping in browsers and Node.js. Temporal provides immutable, timezone-aware date and time types with clear separation between human calendar time and exact machine timestamps. This guide covers Temporal’s core types, timezone handling, date arithmetic, and migration strategies.

Why Temporal Replaces the Date Object

JavaScript’s Date object has accumulated decades of design flaws that make it one of the most criticized parts of the language. Months are zero-indexed — January is 0, December is 11 — causing off-by-one errors that consistently rank among the most common JavaScript bugs. Date.parse() implementation varies across browsers, producing inconsistent results for identical date strings. Mutable methods like setDate() modify the original object, creating subtle bugs when dates are passed between functions in asynchronous code.

Timezone support in the Date object is essentially nonexistent. The object always represents an instant internally in UTC, and the toString() method converts to local time according to the system timezone. There is no way to represent a date without a time, a time without a date, or a date-time in a specific timezone without resorting to string manipulation. Libraries like Moment.js (now legacy), date-fns, and Luxon emerged to fill these gaps, but Temporal provides first-class solutions at the language level.

Temporal’s fundamental design principles are immutability, type safety, and precision. All Temporal objects are immutable — methods return new instances rather than modifying the original, eliminating the mutation bugs common with Date. Types are distinct and explicit — you cannot accidentally add a Duration to an Instant when you meant to add it to a PlainDate. Nanosecond precision handles the most demanding use cases in scientific computing and financial transactions.

Temporal Core Types

Temporal organizes time into distinct types that represent different concepts with clear separation. Temporal.PlainDate represents a calendar date without time or timezone — “2024-03-15”. Use it for birthdays, holidays, deadlines, and any date-only concept. Temporal.PlainTime represents a wall-clock time without date or timezone — “14:30:00”. Use it for opening hours, alarm times, and recurring daily events.

Temporal.PlainDateTime combines date and time without a timezone — “2024-03-15T14:30:00”. It represents a local date-time that hasn’t been anchored to a specific timezone. This type is useful for scheduling concepts where the timezone will be determined later, or for representing times in a fixed-offset timezone.

Temporal.ZonedDateTime anchors a date-time to a specific IANA timezone — “2024-03-15T14:30:00[America/New_York]”. This type understands Daylight Saving Time transitions, historical timezone changes, and UTC offset variations. Use ZonedDateTime for event scheduling, meeting times, flight departures, and any concept that needs timezone-aware comparison.

Temporal.Instant represents an exact point on the timeline independent of timezones and calendars. It corresponds to a Unix timestamp with nanosecond precision. Use Instant for logs, timestamps, event ordering, and any data that represents a concrete moment in time regardless of where it’s observed.

Temporal.Duration represents a length of time in ISO 8601 format — “P1Y2M3DT4H5M6S”. Duration arithmetic correctly handles variable-length units — adding one month to January 31 produces February 28 or 29, not March 3. Temporal.TimeZone represents a specific IANA timezone with methods for UTC offset calculation and DST transition lookup.

Timezone Handling and DST

Timezone conversions that break other date libraries are straightforward in Temporal. Convert a meeting time from New York to London — meeting.withTimeZone("Europe/London") — and Temporal correctly calculates the precise moment based on the UTC offset difference, accounting for DST in both timezones. This single method handles the complexity that requires multiple library calls in date-fns or manual offset calculations in JavaScript’s Date.

Daylight Saving Time transitions create ambiguous and nonexistent times that Temporal handles explicitly. When clocks spring forward, the hour from 2:00 AM to 3:00 AM doesn’t exist — times in this gap are “nonexistent.” When clocks fall back, the hour from 1:00 AM to 2:00 AM occurs twice — times in this range are “ambiguous.” Temporal’s disambiguation option controls behavior: "earlier" selects the first occurrence, "later" selects the second, "reject" throws an error, and "compatible" (the default) matches legacy behavior by selecting "later" for fall-back and "earlier" for spring-forward.

The Temporal.Now namespace provides the current time in various representations. Temporal.Now.instant() returns the current Instant, replacing Date.now(). Temporal.Now.zonedDateTimeISO() returns the current date-time in the system timezone, replacing new Date(). Temporal.Now.timeZoneId() returns the system timezone identifier string.

Date and Time Arithmetic

Temporal arithmetic handles the complexities that make Date calculations error-prone. Adding months crosses month boundaries correctly — date.add({ months: 1 }) from January 31 produces February 28 (or 29 in leap years), not March 3 as JavaScript’s naive month addition would produce. Adding years correctly handles leap years — date.add({ years: 1 }) from February 29, 2024 produces February 28, 2025.

Comparison methods use the static compare() method. Temporal.PlainDate.compare(a, b) returns -1 if a is before b, 0 if equal, and 1 if a is after b. Comparison works across Temporal types — compare PlainDate with PlainDateTime, ZonedDateTime with Instant, with automatic timezone offset handling.

The since() and until() methods calculate durations between two temporal values. date1.until(date2, { largestUnit: 'months' }) returns the duration from date1 to date2. round() rounds durations to specified precision. duration.round({ smallestUnit: 'minutes' }) rounds seconds and sub-seconds. Balance methods normalize durations across units — duration.round({ largestUnit: 'days' }) converts 36 hours to 1 day and 12 hours.

Formatting and Locale Support

Temporal integrates with the existing Intl.DateTimeFormat API for locale-aware formatting. Pass a Temporal object directly to the formatter — new Intl.DateTimeFormat('de-DE', { dateStyle: 'full' }).format(zdt) — and it correctly handles timezone offsets and locale-specific formatting patterns. Temporal objects implement the necessary interfaces for seamless integration.

Custom formatting uses Temporal’s own toString() options with fine-grained control. zonedDateTime.toString({ calendarName: 'always', offset: 'always', timeZoneName: 'long' }) produces detailed string representations like “2024-03-15T14:30:00-04:00[America/New_York][u-ca=gregory]”. The toLocaleString() method provides quick locale-aware output.

Calendar support extends beyond the ISO 8601 calendar to include religious and cultural calendars. Temporal supports Hebrew, Islamic (tabular), Chinese, Persian, Japanese, Indian, Buddhist, and Ethiopian calendars through the calendar option. Temporal.PlainDate.from({ year: 5784, month: 7, day: 15, calendar: 'hebrew' }) creates a date in the Hebrew calendar.

Migration from the Date Object

Migrating existing code to Temporal requires systematic replacement of common patterns. Replace new Date() for current time with Temporal.Now.instant() for timestamps or Temporal.Now.zonedDateTimeISO() for the current date-time in the local timezone. Replace new Date('2024-03-15') for specific dates with Temporal.PlainDate.from('2024-03-15'). Replace date.getTime() with instant.epochMilliseconds for Unix timestamps in milliseconds. Replace date.getFullYear(), date.getMonth(), date.getDate() with plainDate.year, plainDate.month, plainDate.day properties.

Timestamp arithmetic with Date — like adding days or months — requires replacing manual calculations with Temporal’s type-safe Duration methods. Instead of const tomorrow = new Date(date.setDate(date.getDate() + 1)) (which mutates the original), use date.add({ days: 1 }) which returns a new instance.

Timezone conversions replace the error-prone toLocaleString('en-US', { timeZone: 'America/New_York' }) pattern with zonedDateTime.withTimeZone(targetTimeZone). Date formatting uses Temporal objects directly with Intl.DateTimeFormat.

Library wrappers like temporal-polyfill enable Temporal in environments without native support. The polyfill implements the full Temporal specification in JavaScript with identical API surface and is suitable for production use. It can be removed when native support becomes universal in your target environments.

Working with Durations and Periods

Temporal.Duration represents a length of time that can be positive or negative. Create durations with Temporal.Duration.from({ hours: 5, minutes: 30 }) or from ISO 8601 strings like Temporal.Duration.from('PT5H30M'). Duration arithmetic handles variable-length units correctly — adding one month to January 31 produces February 28 or 29, not March 3.

The round() method rounds durations to specified precision: duration.round({ smallestUnit: 'minutes' }) rounds seconds and sub-seconds. The total() method converts duration to a specified unit: duration.total('hours') returns total hours as a floating point number. The sign property indicates positive or negative direction.

Duration balancing converts overflowing units into larger units. duration.round({ largestUnit: 'days' }) converts 36 hours to 1 day and 12 hours. Without balancing, durations maintain their original unit structure.

Non-ISO Calendar Support

Temporal supports calendar systems beyond the ISO 8601 Gregorian calendar used in most business applications. The built-in calendar identifiers include 'chinese', 'hebrew', 'islamic', 'persian', 'japanese', 'indian', 'buddhist', and 'ethiopic'. Each calendar maintains its own rules for months, years, and era boundaries.

Creating dates in non-ISO calendars uses the calendar option: Temporal.PlainDate.from({ year: 5784, month: 7, day: 15, calendar: 'hebrew' }). The calendarId property on any Temporal object returns the calendar identifier. Calendar-aware arithmetic respects the calendar’s leap month rules and era boundaries.

Calendar conversion between systems uses date.withCalendar('iso8601') to get the ISO equivalent. The era and eraYear properties provide calendar-specific era information — useful for Japanese era names and Buddhist calendar display.

FAQ

When will Temporal be available in browsers? Temporal reached Stage 3 and is landing in browsers. Chrome 124+ includes Temporal behind a flag. Production use via polyfill is safe.

Is Temporal backward compatible with the Date object? Temporal is a new separate API — it doesn’t modify the existing Date object. Code using new Date() continues working unchanged.

How does Temporal handle timezone database updates? Temporal uses the ICU timezone database included with the JavaScript runtime. Browser and Node.js updates include timezone data changes.

What is the difference between PlainDate and ZonedDateTime? PlainDate represents a calendar date without time or timezone. ZonedDateTime represents a specific moment in a specific timezone.

Can Temporal handle nanoseconds? Yes. Temporal’s Instant type supports nanosecond precision through epochNanoseconds.

For more JavaScript fundamentals, explore our JavaScript modules guide and Fetch API guide.

Section: JavaScript 1569 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top