Skip to main content

Temporal API in JavaScript

The Temporal API is a new standard set of date and time types for JavaScript, meant to replace Date. It is a TC39 proposal that reached Stage 3, is already implemented in some engines and is available everywhere through the official polyfill. The core idea: immutable objects, a clear separation of the concepts "instant", "date", "time" and "time zone", plus accurate handling of the IANA time zone database.

Theory

TL;DR

  • Temporal is a new built-in date and time API; it does not extend Date, it replaces it.
  • Every object is immutable: add(), subtract(), with() and round() return a new value.
  • Separate types for separate jobs: Instant, PlainDate, PlainTime, PlainDateTime, ZonedDateTime, Duration, Now.
  • The time zone is stored as part of the value ([Europe/Kyiv]), not just as a +03:00 offset, so daylight saving transitions are handled correctly.
  • An invalid date raises a RangeError (with overflow: 'reject') or is clamped explicitly, instead of silently rolling over as new Date(2025, 1, 30) does.
  • Months are numbered from 1, not from 0 as in Date.
  • Formatting is delegated to Intl.DateTimeFormat; older environments pull in @js-temporal/polyfill.

Quick example

javascript
// A date with no time const date = Temporal.PlainDate.from('2025-10-16'); console.log(date.year, date.month, date.day); // 2025 10 16 // A time with no date const time = Temporal.PlainTime.from('13:45:30'); console.log(time.hour); // 13 // A date and time with no time zone const dt = Temporal.PlainDateTime.from('2025-10-16T13:45:30'); console.log(dt.toString()); // 2025-10-16T13:45:30

Why Date is not enough

Date is one of the oldest and most problematic parts of the language: its API was copied from Java back in 1995 and has barely changed since.

ProblemExample
Implicit time zonesnew Date('2024-01-01') is parsed as UTC, while new Date('2024-01-01T00:00') is parsed as local time
Unpredictable arithmeticnew Date(2024, 1, 30) gives 1 March, because 30 February does not exist
Mutating methodssetMonth(), setHours() change the original object instead of returning a new one
Clumsy UTC handlingyou juggle toISOString() and the getHours() / getUTCHours() pairs by hand
Zero based numberingmonths are 0..11 while days of the month are 1..31
No types for calendars, durations or zonesevery small task pulled in a third party library: Moment, date-fns, Luxon

That is why Temporal does not patch Date: it introduces a separate, consistent model of time.

The Temporal classes

ClassWhat it representsExample
Temporal.Instanta moment on the absolute timeline (a point in UTC)"2025-10-16T21:00:00Z"
Temporal.PlainDatea date with no time"2025-10-16"
Temporal.PlainTimea time with no date"21:00:00"
Temporal.PlainDateTimea date and time with no time zone"2025-10-16T21:00:00"
Temporal.ZonedDateTimedate + time + time zone"2025-10-16T21:00:00+03:00[Europe/Kyiv]"
Temporal.Durationa length of time, the difference between two moments"P3DT5H" (3 days 5 hours)
Temporal.Nowa helper for reading the current timeTemporal.Now.instant()

The difference between PlainDateTime and ZonedDateTime is the key interview point. PlainDateTime is wall clock time with no place attached: "16 October at 21:00" means different instants in different countries. ZonedDateTime carries the zone, so it points at one specific instant and knows about DST transitions. When you need an actual instant (an event timestamp, a log record), use Instant or ZonedDateTime; when you need a birthday or an office opening time, use PlainDate / PlainTime.

Time zones and daylight saving time

javascript
const zdt = Temporal.ZonedDateTime.from({ timeZone: 'Europe/Kyiv', year: 2025, month: 10, day: 16, hour: 21, }); console.log(zdt.toString()); // 2025-10-16T21:00:00+03:00[Europe/Kyiv] console.log(zdt.toInstant().toString()); // 2025-10-16T18:00:00Z (the same moment in UTC)

Temporal keeps the zone identifier ([Europe/Kyiv]) as part of the data, not just the +03:00 offset. Because of that, arithmetic respects daylight saving transitions:

javascript
const beforeDST = Temporal.ZonedDateTime.from('2025-03-30T01:30:00+01:00[Europe/Berlin]'); const afterDST = beforeDST.add({ hours: 1 }); console.log(afterDST.toString()); // 2025-03-30T03:30:00+02:00[Europe/Berlin]

The clock jumped from 01:30 to 03:30 because the clocks moved forward at 02:00: one real hour was added and the offset changed from +01:00 to +02:00.

The current time comes from Temporal.Now:

javascript
const now = Temporal.Now.instant(); console.log(now.toString()); // the current time in UTC const kyiv = Temporal.Now.zonedDateTimeISO('Europe/Kyiv'); console.log(kyiv.toString()); // for example: 2025-10-16T23:50:00+03:00[Europe/Kyiv]

Arithmetic, Duration and strict validation

Every operation returns a new value and leaves the original untouched:

javascript
const today = Temporal.PlainDate.from('2025-10-16'); const tomorrow = today.add({ days: 1 }); const lastWeek = today.subtract({ weeks: 1 }); console.log(tomorrow.toString()); // 2025-10-17 console.log(lastWeek.toString()); // 2025-10-09 console.log(today.toString()); // 2025-10-16, unchanged

The difference between two moments is a Temporal.Duration in ISO 8601 form:

javascript
const start = Temporal.PlainDateTime.from('2025-10-16T10:00'); const end = Temporal.PlainDateTime.from('2025-10-18T15:30'); const duration = end.since(start); console.log(duration.toString()); // P2DT5H30M (2 days 5 hours 30 minutes) console.log(duration.days, duration.hours, duration.minutes); // 2 5 30

Invalid values are never fixed up behind your back. A string holding a date that does not exist throws right away, and for an object the overflow option decides:

javascript
// The old Date silently rolls the date over new Date(2025, 1, 30); // 2025-03-02 (month 1 is February, and 30 February does not exist) // Temporal: a string with a non existent date is an immediate error Temporal.PlainDate.from('2025-02-30'); // RangeError // For an object the default is overflow: 'constrain' (clamped to the end of the month) Temporal.PlainDate.from({ year: 2025, month: 2, day: 30 }); // 2025-02-28 // Strict mode Temporal.PlainDate.from({ year: 2025, month: 2, day: 30 }, { overflow: 'reject' }); // RangeError

Note that in Temporal month: 2 is February, because months are numbered from 1. In Date the same position would mean March.

Benefits, formatting and support

BenefitWhat it gives you
Immutabilitysafe operations with no mutation and no accidental side effects
A clear modelthe concepts "instant", "date", "time" and "zone" are kept apart
Accurate time zonesthe IANA database, correct DST, the zone held inside the value
No Date magicerrors instead of implicit corrections
Works with Intluser facing formatting without hand built strings
ISO 8601 and Durationsimple work with differences and intervals

Formatting is delegated to Intl.DateTimeFormat, so localisation looks familiar:

javascript
const zdt = Temporal.ZonedDateTime.from('2025-10-16T21:00:00+03:00[Europe/Kyiv]'); const fmt = new Intl.DateTimeFormat('en-GB', { dateStyle: 'full', timeStyle: 'long' }); console.log(fmt.format(zdt)); // Thursday 16 October 2025 at 21:00:00 Eastern European Summer Time

Where you can already use it:

  • The newest engines ship Temporal natively (Firefox was first with a complete implementation), the rest are working on it.
  • In Node.js and older browsers you pull in the official polyfill.
  • Before shipping to production check support on MDN or guard with typeof Temporal !== 'undefined'.
bash
npm i @js-temporal/polyfill
javascript
import { Temporal } from '@js-temporal/polyfill'; const date = Temporal.PlainDate.from('2025-10-16'); console.log(date.toString()); // 2025-10-16

Common mistakes

  • Expecting mutation. dt.add({ days: 1 }) changes nothing in dt; you have to assign the result. This is the most common slip for anyone used to setDate().
  • Confusing PlainDateTime with ZonedDateTime. PlainDateTime is not a moment on the timeline: without a zone it cannot be compared to an Instant or stored as an event timestamp.
  • Storing only the offset. +03:00 is not a time zone: a week after the switch back to winter time the same place is at +02:00. Store the IANA identifier (Europe/Kyiv).
  • Zero based months. Porting code from Date it is easy to leave month: 0 in place; in Temporal that is a RangeError, because January is 1.
  • Counting on a RangeError where constrain applies. By default from({...}) clamps the value to the end of the month; pass { overflow: 'reject' } if you want an error.
  • Mixing Date and Temporal in calculations. Convert explicitly with date.toTemporalInstant() and instant.epochMilliseconds.
  • Pulling in a heavy library just for Temporal. The polyfill is not small either, so for a single formatting call plain Date plus Intl.DateTimeFormat may be enough.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.