# JavaScript Temporal API Tutorial: Replace Date with Tested Examples (2026)

**Temporal is JavaScript’s new built-in date and time API, designed to replace the old `Date` object.** Instead of one confusing type, it gives you separate, immutable types for each job: `Temporal.PlainDate` for a calendar date such as a birthday, `Temporal.ZonedDateTime` for an exact moment in a named time zone, `Temporal.Duration` for lengths of time, and `Temporal.Instant` for a timestamp. Months are numbered 1 to 12, adding a month to 31 January gives 28 February instead of 2 March, and time zones are handled properly.

Temporal is trending because it has finally shipped in mainstream runtimes. [Node.js 26.0.0](https://nodejs.org/en/blog/release/v26.0.0/), released on 5 May 2026, turned it on by default, and Node 26 is due to become the long-term support (LTS) release this October. Chrome 144 (13 January 2026) and Firefox 139 (27 May 2025) also support it. I ran every example below on **Node.js 26.11.1** on 12 October 2026 and pasted the real output.

## Why replace Date?

`Date` has been in JavaScript since 1995 and has well-known problems:

- **Months start at 0.** `new Date(2026, 0, 31)` is 31 January, not a mistake you’d guess.
- **It’s mutable.** `setMonth()` changes the object in place, so passing a date to a function can silently change it for the caller.
- **Only two time zones.** A `Date` only knows UTC and the computer’s local zone. Showing a meeting time in London and Dhaka needs extra libraries.
- **Surprising arithmetic.** Here’s what happens when you add a month to 31 January with `Date`:

```js
const old = new Date(2026, 0, 31);
old.setMonth(old.getMonth() + 1);
console.log(old.toISOString().slice(0, 10));
```

```text
2026-03-02
```

There’s no 31 February, so `Date` rolls over into March. Temporal’s answer to the same question is coming up below. If you just need to format an existing `Date`, my guide to [formatting a date as dd/mm/yyyy in JavaScript](https://mfaysal.com/blog/javascript-format-date-dd-mm-yyyy) still works; Temporal is for when you need real date maths.

## Check that Temporal is available

`Temporal` is a global object, like `Math`. Feature-detect it before relying on it:

```js
console.log(typeof globalThis.Temporal);
```

On Node.js 26 this printed `object`. On the box’s older Node.js 20 it printed `undefined`. Browser support, from the [web-features explorer](https://web-platform-dx.github.io/web-features-explorer/features/temporal/) on 12 October 2026, is:

- **Chrome and Edge:** supported from version 144 (January 2026).
- **Firefox:** supported from version 139 (May 2025).
- **Safari (macOS and iOS):** not supported yet, which is why Temporal is still marked “limited availability” rather than Baseline.

For public websites, that Safari gap matters. Use a polyfill such as `@js-temporal/polyfill` or `temporal-polyfill` when `globalThis.Temporal` is missing, or keep Temporal to server-side code on Node 26 for now.

## The Temporal types at a glance

Picking the right type is most of the work. Here’s the short version:

- **`Temporal.PlainDate`:** a calendar date with no time or zone. Birthdays, deadlines, bank holidays.
- **`Temporal.PlainTime`:** a wall-clock time with no date. Opening hours, alarm times.
- **`Temporal.PlainDateTime`:** a date and time with no zone. Rarely the right choice for real events, but useful for form inputs before you know the zone.
- **`Temporal.ZonedDateTime`:** an exact moment plus a time zone such as `Europe/London`. Meetings, flights, anything that happens at a real moment for people in a place.
- **`Temporal.Instant`:** an exact moment with no zone, like a timestamp in a database.
- **`Temporal.Duration`:** a length of time, like “2 months and 2 days”.
- **`Temporal.PlainYearMonth` and `Temporal.PlainMonthDay`:** “February 2026” or “25 December” on their own.

## PlainDate: working with calendar dates

Create a date from an ISO string (`YYYY-MM-DD`) and read its parts:

```js
const d = Temporal.PlainDate.from("2026-10-12");
console.log(d.toString(), d.year, d.month, d.day, d.dayOfWeek, d.daysInMonth, d.inLeapYear);
```

```text
2026-10-12 2026 10 12 1 31 false
```

October is month **10**, not 9. `dayOfWeek` runs from 1 (Monday) to 7 (Sunday), so 12 October 2026 is a Monday. Useful properties like `daysInMonth` and `inLeapYear` are built in.

To get today’s date in the computer’s time zone, use `Temporal.Now.plainDateISO()`. For the current exact moment, use `Temporal.Now.instant()`.

### Start and end of the month

`with()` returns a copy with some fields replaced:

```js
console.log(d.with({ day: 1 }).toString(), d.with({ day: d.daysInMonth }).toString());
```

```text
2026-10-01 2026-10-31
```

## Adding days and months (and the 31 January problem)

Temporal objects are **immutable**: `add()` and `subtract()` return a new object and leave the original alone.

```js
const start = Temporal.PlainDate.from("2026-10-12");
const later = start.add({ days: 7 });
console.log(start.toString(), later.toString());
```

```text
2026-10-12 2026-10-19
```

Now the same month problem that tripped up `Date`:

```js
const jan31 = Temporal.PlainDate.from("2026-01-31");
console.log(jan31.add({ months: 1 }).toString());
try {
  jan31.add({ months: 1 }, { overflow: "reject" });
} catch (e) {
  console.log(e.constructor.name + ":", e.message);
}
```

```text
2026-02-28
RangeError: Temporal error: not a valid ISO date.
```

By default Temporal **constrains** the result to the last valid day, 28 February, which is what most people expect for “one month later”. If you’d rather catch the problem, pass `{ overflow: "reject" }` and you get a `RangeError` instead. The exact error wording comes from Node’s engine and may differ in browsers.

## Differences between dates: until() and since()

`until()` returns a `Temporal.Duration`. By default the largest unit for dates is days, but you can ask for months or years:

```js
const exam = Temporal.PlainDate.from("2026-12-14");
console.log(start.until(exam).toString(), start.until(exam, { largestUnit: "months" }).toString(), start.until(exam).days);
```

```text
P63D P2M2D 63
```

`P63D` is the ISO 8601 way of writing “63 days”, and `P2M2D` is “2 months, 2 days”. Read the parts with properties such as `.days` and `.months`.

### Working out someone’s age

Age calculations are notoriously fiddly with `Date`. With Temporal, ask for years:

```js
const birth = Temporal.PlainDate.from("2004-02-29");
console.log(birth.until(Temporal.PlainDate.from("2026-10-12"), { largestUnit: "years" }).years);
```

```text
22
```

Even a leap-day birthday works without special-casing.

## Comparing and sorting dates

Use `Temporal.PlainDate.compare` as a sort function and `equals()` for equality. Don’t use `===`, which compares object identity, not dates:

```js
const dates = ["2026-12-01", "2026-01-15", "2026-06-30"].map(s => Temporal.PlainDate.from(s));
console.log(dates.sort(Temporal.PlainDate.compare).map(String).join(", "), start.equals(Temporal.PlainDate.from("2026-10-12")));
```

```text
2026-01-15, 2026-06-30, 2026-12-01 true
```

For skipping weekends and bank holidays, the logic in my post on [counting UK working days in JavaScript](https://mfaysal.com/blog/javascript-working-days-uk-bank-holidays) becomes simpler with `dayOfWeek` and `add({ days: 1 })`.

## ZonedDateTime: time zones done properly

This is where Temporal really shines. Create a moment in a named IANA time zone, then view it in other zones:

```js
const meet = Temporal.ZonedDateTime.from("2026-10-12T15:00[Europe/London]");
console.log(meet.toString());
console.log(meet.withTimeZone("Asia/Dhaka").toString());
console.log(meet.withTimeZone("America/New_York").toPlainTime().toString());
```

```text
2026-10-12T15:00:00+01:00[Europe/London]
2026-10-12T20:00:00+06:00[Asia/Dhaka]
10:00:00
```

A 3 pm meeting in London (on British Summer Time, +01:00) is 8 pm in Dhaka and 10 am in New York. The string keeps both the offset and the zone name, so it can be stored and read back without losing information.

### Daylight saving time: days vs hours

UK clocks go back on Sunday 25 October 2026. Watch what happens when you add “1 day” versus “24 hours” across that change:

```js
const beforeDst = Temporal.ZonedDateTime.from("2026-10-24T12:00[Europe/London]");
console.log(beforeDst.add({ days: 1 }).toString(), beforeDst.add({ hours: 24 }).toString());
```

```text
2026-10-25T12:00:00+00:00[Europe/London] 2026-10-25T11:00:00+00:00[Europe/London]
```

Adding a **day** keeps the same wall-clock time (noon). Adding **24 hours** lands at 11:00, because that particular day was 25 hours long. Temporal makes you choose which one you mean, which is exactly the kind of bug `Date` hides. The same issue exists on the server side; I covered it for PHP in [setting the timezone to Europe/London in PHP](https://mfaysal.com/blog/php-timezone-europe-london).

## Durations

`Temporal.Duration` represents a length of time. It doesn’t automatically “balance” units, but `round()` and `total()` will:

```js
const dur = Temporal.Duration.from({ hours: 1, minutes: 90 });
console.log(dur.toString(), dur.round({ largestUnit: "hours" }).toString(), dur.total({ unit: "minutes" }));
```

```text
PT1H90M PT2H30M 150
```

`PT1H90M` is kept exactly as you wrote it, rounding gives `PT2H30M` (2 hours 30 minutes), and `total()` converts it to a single number of minutes.

## Plain times, date-times and month-days

The other Plain types follow the same pattern:

```js
console.log(Temporal.PlainTime.from("09:15").add({ minutes: 50 }).toString());
console.log(Temporal.PlainDateTime.from("2026-10-12T23:30").add({ hours: 1 }).toString());
console.log(Temporal.PlainYearMonth.from("2026-02").daysInMonth, Temporal.PlainMonthDay.from("12-25").toPlainDate({ year: 2026 }).dayOfWeek);
```

```text
10:05:00
2026-10-13T00:30:00
28 5
```

A 50-minute lesson starting at 09:15 ends at 10:05. Adding an hour to 23:30 rolls into the next day. February 2026 has 28 days, and Christmas Day 2026 is a Friday (day 5).

## Converting between Temporal and Date

You’ll be mixing old and new code for a while, so conversions matter. An `Instant` maps cleanly to and from epoch milliseconds, and `Date` has a new `toTemporalInstant()` method:

```js
const inst = Temporal.Instant.fromEpochMilliseconds(Date.UTC(2026, 9, 12, 9, 30));
console.log(inst.toString(), inst.epochMilliseconds, new Date(inst.epochMilliseconds).toISOString());
console.log(new Date("2026-10-12T09:30:00Z").toTemporalInstant().toString());
```

```text
2026-10-12T09:30:00Z 1791797400000 2026-10-12T09:30:00.000Z
2026-10-12T09:30:00Z
```

Note the old `Date.UTC(2026, 9, ...)` needs month 9 for October. That’s the zero-based month problem again, and one more reason to move to Temporal.

## Formatting dates for people

Every Temporal type has `toLocaleString()`, which uses the same `Intl` options you may know from `Date`:

```js
console.log(d.toLocaleString("en-GB", { dateStyle: "full" }));
console.log(meet.toLocaleString("en-GB", { dateStyle: "medium", timeStyle: "short" }));
```

```text
Monday, 12 October 2026
12 Oct 2026, 15:00
```

One gotcha I hit: combining `dateStyle` or `timeStyle` with individual options such as `timeZoneName` throws `TypeError: Invalid option`. That’s an `Intl.DateTimeFormat` rule, not a Temporal bug. Use either the style shortcuts or individual fields like `{ day: "numeric", month: "long", timeZoneName: "short" }`.

## Validation: rejecting bad dates

Temporal refuses to parse impossible ISO strings and non-ISO formats:

```js
try { Temporal.PlainDate.from("2026-02-30", { overflow: "reject" }); }
catch (e) { console.log(e.constructor.name + ":", e.message); }
try { Temporal.PlainDate.from("12/10/2026"); }
catch (e) { console.log(e.constructor.name + ":", e.message); }
```

```text
RangeError: Temporal error: Parsed day value not in a valid range.
RangeError: Temporal error: Invalid character while parsing year value.
```

In my tests, `"2026-02-30"` was rejected even without the `overflow` option, because ISO strings are validated strictly. If you build a date from separate fields, though, the default `overflow: "constrain"` clamps it: `Temporal.PlainDate.from({ year: 2026, month: 2, day: 30 })` gave `2026-02-28`. For user input from a form, parse the parts yourself and pass `{ overflow: "reject" }` so mistakes are caught rather than silently adjusted.

`"12/10/2026"` fails outright. That’s a feature: it’s ambiguous (12 October or 10 December?), and Temporal won’t guess.

## Two practical examples

### Adding working days

Skipping weekends is a loop over `dayOfWeek` (Saturday is 6, Sunday is 7):

```js
function addWorkingDays(date, n) {
  let d = date;
  while (n > 0) {
    d = d.add({ days: 1 });
    if (d.dayOfWeek <= 5) n--;
  }
  return d;
}
const friday = Temporal.PlainDate.from("2026-10-16");
console.log(friday.dayOfWeek, addWorkingDays(friday, 3).toString());
```

```text
5 2026-10-21
```

Three working days after Friday 16 October is Wednesday 21 October. Because `add()` never mutates, there’s no risk of changing the caller’s `friday` by accident.

### A countdown across time zones

Here’s a countdown from 09:15 in Dhaka to a launch at 18:00 in London. My first attempt threw an error worth knowing about:

```text
RangeError: Temporal error: Timezones must be the same if unit is a day unit.
```

Days can be 23 or 25 hours long depending on the zone, so Temporal refuses to count days between two different zones. Convert one side first:

```js
function countdown(now, deadline) {
  const left = now.until(deadline.withTimeZone(now.timeZoneId), { largestUnit: "days", smallestUnit: "minutes" });
  return `${left.days}d ${left.hours}h ${left.minutes}m`;
}
const now = Temporal.ZonedDateTime.from("2026-10-12T09:15[Asia/Dhaka]");
const launch = Temporal.ZonedDateTime.from("2026-10-20T18:00[Europe/London]");
console.log(countdown(now, launch));
console.log(JSON.stringify({ launch }));
```

```text
8d 13h 45m
{"launch":"2026-10-20T18:00:00+01:00[Europe/London]"}
```

`JSON.stringify` uses the full string with offset and zone, and `Temporal.ZonedDateTime.from()` reads it back to an equal value, so storing Temporal values in JSON or a text column is lossless.

## Common Temporal mistakes

- **Using PlainDateTime for real events.** Without a zone, “2026-10-25T01:30” is ambiguous on the night the clocks change. Use `ZonedDateTime`.
- **Comparing with `===` or `<`.** Use `equals()` and `compare()`.
- **Expecting Duration to balance itself.** Call `round()` or `total()`.
- **Forgetting Safari.** Feature-detect and polyfill for browser code.
- **Assuming error messages are identical everywhere.** Catch `RangeError` by type, not by message text.
- **Counting days between two different time zones.** Convert one value with `withTimeZone()` first, as in the countdown example.
- **Storing a PlainDateTime when you meant an Instant.** For logs and database timestamps, store an `Instant` (or a `ZonedDateTime` string) so the exact moment is never lost.

## FAQ

### What is the Temporal API in JavaScript?

Temporal is a built-in global object that provides modern, immutable date and time types, including `PlainDate`, `ZonedDateTime`, `Instant` and `Duration`. It’s designed to replace the older `Date` object.

### Is Temporal supported in Node.js?

Yes. Node.js 26.0.0, released on 5 May 2026, enables Temporal by default with no flag. Node.js 20 and older don’t have it, so `typeof Temporal` returns `"undefined"` there.

### Which browsers support Temporal?

As of 12 October 2026, Chrome and Edge 144+ and Firefox 139+ support it. Safari on macOS and iOS doesn’t yet, so use a polyfill for public websites.

### What is the difference between PlainDate and ZonedDateTime?

`PlainDate` is just a calendar date with no time or time zone, like a birthday. `ZonedDateTime` is an exact moment in a specific time zone, such as a meeting at 15:00 in Europe/London.

### How do I add months to a date in JavaScript without bugs?

Use `Temporal.PlainDate.from("2026-01-31").add({ months: 1 })`, which returns 28 February 2026. Pass `{ overflow: "reject" }` if you’d rather get an error than an adjusted date.

### How do I convert a Date to Temporal?

Call `date.toTemporalInstant()` to get a `Temporal.Instant`, then `instant.toZonedDateTimeISO("Europe/London")` if you need a time zone. To go back, use `new Date(instant.epochMilliseconds)`.

### Are Temporal objects mutable?

No. Every Temporal object is immutable. Methods such as `add()`, `subtract()` and `with()` return a new object and leave the original unchanged.

---

*Originally published on [mfaysal.com](https://mfaysal.com/blog/javascript-temporal-api-tutorial).*

I'm learning cybersecurity on TryHackMe — follow my progress at [tryhackme.com/p/mfaysal.dev](https://tryhackme.com/p/mfaysal.dev).

