Temporal
Learn the modern date and time API that fixes Date by choosing the right Temporal type for instants, plain calendar values, time zones, durations, calendars, and Date migration.
- 01Pick the right Temporal typeSeparate exact instants, plain calendar values, wall-clock times, and zoned times.
- 02Do date math safelyUse immutable values, durations, and calendar-aware arithmetic instead of mutating Date objects.
- 03Migrate from DateConvert legacy Date timestamps into Temporal.Instant and choose clearer replacements for common Date tasks.
Temporal is a toolbox for time
INTERACTIVEJavaScript's old Date object is one Swiss-army knife that tries to be everything: a timestamp, a calendar date, a local clock time, and a formatter. It works, but it is easy to cut yourself with mutable setters, 0-based months, parsing rules, and daylight-saving surprises. The Date & time lesson introduces those pain points.
Temporal is the modern date and time API that fixes Date by giving each job a specialized type. It is a new built-in, and browsers are shipping it gradually. This page feature-detects it in your browser after mount. When it is available, the live cards run real Temporal code. When it is not, they clearly show example output from a Temporal-enabled engine, checked by our tests with Node's --harmony-temporal flag.
const hasTemporal = typeof globalThis.Temporal !== "undefined";if (hasTemporal) { Temporal.Now.plainDateISO().toString();}Temporal in your browser: checking after mount
Sample: Not checked yet
Checking after mount so the server-rendered page does not guess what your browser supports.
--harmony-temporal for the implementation our tests use.A Swiss-army knife is clever because one object does many jobs. But if you are building a cabinet, you want a real saw, a real hammer, and a real screwdriver. Temporal gives you those dedicated tools for time.
- In real life: A stopwatch reading
- In JavaScript:
Temporal.Instant: one exact timeline point - In real life: A birthday on a paper calendar
- In JavaScript:
Temporal.PlainDate: date with no time or place - In real life: A clock face for a daily alarm
- In JavaScript:
Temporal.PlainTime: time with no date - In real life: A flight leaving Mumbai at local 09:00
- In JavaScript:
Temporal.ZonedDateTime: date, time, and place rules - In real life: A timer set for 90 minutes
- In JavaScript:
Temporal.Duration: a length of time
Where the analogy stops: Real toolboxes can still contain the wrong tool. Temporal helps by naming the tools clearly, but you still choose the type that matches your data.
Sort these real situations by the Temporal type they need.
- A server log timestamp:
2024-03-10T06:30:00Z - A birthday: May 12, with no year needed for the reminder
- The store opens every day at
09:00 - A meeting in Bengaluru at 10:00 on June 3
- A monthly billing period: February 2024
- A countdown length of 1 hour 30 minutes
Put each scenario with the Temporal type that models it best. The explanations are the important part.
Temporal.Now & Instant
Temporal.Now is the doorway to the current time. For deterministic lessons and tests we use fixed values instead, but in an app you might call Temporal.Now.instant() for the exact current moment or Temporal.Now.plainDateISO() for today's ISO calendar date.
An Instant is an exact point on the universal timeline. It is ideal for server logs, database timestamps, cache expiry moments, and anything that must mean the same instant everywhere on Earth.
const instant = Temporal.Instant.from("2024-03-10T06:30:00Z");console.log(instant.toString());console.log(instant.epochMilliseconds);2024-03-10T06:30:00Z
1710052200000
The trailing Z means UTC. The epoch milliseconds are the same kind of number that Date.now() returns, which makes Instant a clean bridge from old APIs.
PlainDate, PlainTime & PlainDateTime
STEP THROUGHPlainDate, PlainTime, and PlainDateTime are called “plain” because they have no time zone. They are calendar or clock values written on paper: a birthday, an opening time, or “January 31 at 09:30” before anyone says where.
const date = Temporal.PlainDate.from("2024-01-31");const time = Temporal.PlainTime.from("09:30:15");const dateTime = Temporal.PlainDateTime.from("2024-01-31T09:30");console.log(date.toString());console.log(date.month);console.log(time.toString());console.log(dateTime.toString());Plain months are 1-based. Temporal.PlainDate.from("2024-01-31").month is 1, not 0. Temporal values are also immutable: methods like add and with return new objects instead of changing the original.
Step through how Temporal keeps date arithmetic immutable. Change the number of months, then predict the new result.
script
const later = original.add({ months: 1 });const firstDay = original.with({ day: 1 });console.log(later.toString());console.log(firstDay.toString());console.log(original.toString());console.log(Temporal.PlainDate.compare(original, later));console.log(original.equals(Temporal.PlainDate.from("2024-01-31")));The default overflow behavior is constrained: if the target month has no day 31, Temporal chooses the last valid day. In leap-year February 2024, that day is the 29th.
PlainYearMonth & PlainMonthDay
Some real data is intentionally incomplete. A credit-card expiry is a year and month. A recurring birthday reminder may need only month and day. Temporal has types for those shapes, so you do not invent fake days or years just to fit a full date.
const expiry = Temporal.PlainYearMonth.from("2024-02");const leapBirthday = Temporal.PlainMonthDay.from("--02-29");console.log(expiry.toString());console.log(leapBirthday.toString());You do not always point to a complete square on a calendar. Sometimes you mean the whole month, and sometimes you mean a recurring day.
- In real life: A page labeled February 2024
- In JavaScript:
PlainYearMonth.from("2024-02") - In real life: A birthday square labeled Feb 29
- In JavaScript:
PlainMonthDay.from("--02-29") - In real life: A full page square with year, month, and day
- In JavaScript:
PlainDate.from("2024-02-29")
Where the analogy stops: A paper calendar cannot compute leap-year rules for you. Temporal can validate and calculate according to its calendar rules.
ZonedDateTime & time zones
STEP THROUGHA time zone is more than an offset like -05:00. It is a named set of rules: daylight-saving changes, historical changes, and local civil time. A ZonedDateTime keeps an exact instant, a wall-clock date and time, and the time zone rules together.
A time zone is not just an offset. Step through why adding 24 hours and adding 1 day can differ.
script
"2024-03-10T01:30-05:00[America/New_York]",);console.log(beforeSpringForward.toString());console.log(beforeSpringForward.add({ hours: 24 }).toString());console.log(beforeSpringForward.add({ days: 1 }).toString());In New York on March 10, 2024, clocks jumped from 01:59:59 to 03:00:00. India does not currently observe daylight saving time, but Indian products serving customers abroad still need to handle transitions like this correctly. That is why adding { hours: 24 } and adding { days: 1 } land at different wall-clock times in the recorded example.
Duration & arithmetic
INTERACTIVEA Duration is a length of time: “3 days” or “1 hour 30 minutes.” You get durations with methods like until and since, or you build one with Temporal.Duration.from. Durations can be balanced, rounded, and totaled when you tell Temporal the unit you want.
const start = Temporal.PlainDate.from("2024-02-28");const end = Temporal.PlainDate.from("2024-03-02");const between = start.until(end);const timer = Temporal.Duration.from({ hours: 1, minutes: 30 });console.log(between.toString());console.log(between.days);console.log(timer.toString());console.log(timer.total({ unit: "minutes" }));P3D3PT1H30M90
Read the code, predict the four lines, then run it.
until creates a Duration from two PlainDates. total converts a Duration into one unit.Dates can produce durations, and durations can be totaled or rounded depending on the unit you need.
script
const end = Temporal.PlainDate.from("2024-03-02");const between = start.until(end);const timer = Temporal.Duration.from({ hours: 1, minutes: 30 });console.log(between.toString());console.log(between.days);console.log(timer.toString());console.log(timer.total({ unit: "minutes" }));Hours and minutes are fixed, but months and years are calendar-sized. When rounding or totaling calendar durations, Temporal may need a starting point so it knows how long that month or year is.
Calendars
Temporal defaults to the ISO calendar used by most programming APIs, but its design can carry other calendars too. Think of calendars as different printed systems for naming dates: Gregorian, Hebrew, Japanese, and more. The time-line instant can be the same while the date label changes.
const isoDate = Temporal.PlainDate.from("2024-05-01");const japaneseDate = isoDate.withCalendar("japanese");console.log(japaneseDate.toString());2024-05-01[u-ca=japanese]
We do not claim era names or localized formatting here because those depend on engine data. The verified output simply shows the calendar annotation carried with the date.
Migrating from Date
INTERACTIVEMigrate by naming what you really have. If you have an exact timestamp, convert it to Instant. If you have a date from a form, parse a PlainDate. If you have a flight departure or meeting, include a time zone.
| Old Date task | Temporal replacement | Why it is clearer |
|---|---|---|
Date.now() | Temporal.Now.instant() | Names an exact current moment instead of a bare number. |
new Date(y, m, d) | Temporal.PlainDate.from({ year, month, day }) | Months are 1-based and the type says no time zone is involved. |
date.toISOString() | instant.toString() | An Instant string is already an ISO timestamp with Z. |
date.setDate(date.getDate() + 1) | plainDate.add({ days: 1 }) | Returns a new value instead of mutating the old one. |
date.getTime() | instant.epochMilliseconds | The timestamp is an explicit property of an Instant. |
const legacyDate = new Date("2024-03-10T06:30:00.000Z");const instant = Temporal.Instant.fromEpochMilliseconds(legacyDate.getTime());console.log(instant.toString());console.log(instant.epochMilliseconds);2024-03-10T06:30:00Z
1710052200000
A Date stores a timestamp. Convert that exact timestamp first, then choose calendar or time-zone types intentionally.
Common misconceptions
“Temporal is available everywhere now.”
Not yet. Feature-detect it. This lesson never loads a polyfill and labels example output when the browser lacks Temporal.
“PlainDate is a date at midnight.”
No. A plain date has no time and no time zone. Midnight in which city would be a different question.
“A time zone is just an offset.”
An offset is one number at one moment. A named time zone carries rules that can change across the year.
“add({ days: 1 }) and add({ hours: 24 }) always match.”
They can differ across DST changes on a ZonedDateTime, as the New York example shows.
“Temporal objects mutate like Date.”
Temporal methods return new values. The original object remains unchanged unless you replace your variable yourself.
| Type | Contains | Use it for |
|---|---|---|
Instant | Exact timeline point | Logs, expiry instants, converting from Date timestamps |
PlainDate | Year, month, day, calendar | Birthdays, due dates, form dates |
PlainTime | Hour, minute, second | Daily alarms, opening hours |
ZonedDateTime | Instant plus local time and time zone | Flights, meetings, deadlines in a place |
Duration | A length of time | Countdowns, differences, arithmetic |
Practice: Temporal
5 EXERCISESRead the code and predict the first printed line.
const original = Temporal.PlainDate.from("2024-01-31");
const later = original.add({ months: 1 });
const firstDay = original.with({ day: 1 });
console.log(later.toString());
console.log(firstDay.toString());
console.log(original.toString());
console.log(Temporal.PlainDate.compare(original, later));
console.log(original.equals(Temporal.PlainDate.from("2024-01-31")));PlainDate.from("2024-01-31").add({ months: 1 }) returns a new date: 2024-02-29. The original remains 2024-01-31.
Which Temporal type fits a server log timestamp such as 2024-03-10T06:30:00Z?
Use Temporal.Instant for a log timestamp. Everyone should agree which exact moment the log line records.
How many days are between the two PlainDates?
const start = Temporal.PlainDate.from("2024-02-28");
const end = Temporal.PlainDate.from("2024-03-02");
const between = start.until(end);
const timer = Temporal.Duration.from({ hours: 1, minutes: 30 });
console.log(between.toString());
console.log(between.days);
console.log(timer.toString());
console.log(timer.total({ unit: "minutes" }));const start = Temporal.PlainDate.from("2024-02-28");
const end = Temporal.PlainDate.from("2024-03-02");
console.log(start.until(end).days);From Feb 28 to Mar 2 in leap year 2024 is three calendar days: Feb 29, Mar 1, then Mar 2.
What Instant string is printed by the migration code?
const legacyDate = new Date("2024-03-10T06:30:00.000Z");
const instant = Temporal.Instant.fromEpochMilliseconds(legacyDate.getTime());
console.log(instant.toString());
console.log(instant.epochMilliseconds);const legacyDate = new Date("2024-03-10T06:30:00.000Z");
const instant = Temporal.Instant.fromEpochMilliseconds(legacyDate.getTime());
console.log(instant.toString());
console.log(instant.epochMilliseconds);The Date holds the exact timestamp 2024-03-10T06:30:00.000Z. Converting its epoch milliseconds gives the Temporal Instant 2024-03-10T06:30:00Z.
Rewrite the code with Temporal so the original start date is not mutated. What due date prints?
const start = new Date("2024-01-31T00:00:00.000Z");
const due = start;
due.setUTCMonth(due.getUTCMonth() + 1);
console.log(start.toISOString()); // start changed tooconst start = Temporal.PlainDate.from("2024-01-31");
const due = start.add({ months: 1 });
console.log(start.toString());
console.log(due.toString());Temporal returns a new PlainDate from add, so start stays 2024-01-31 and due becomes 2024-02-29.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7Which Temporal type means an exact point on the universal timeline?
Choose an answer to see the explanation.
Question 2 of 7What does this print first?
Read the code, then predictconst date = Temporal.PlainDate.from("2024-01-31"); console.log(date.add({ months: 1 }).toString());Choose an answer to see the explanation.
Question 3 of 7Which type should model a credit-card expiry month?
Choose an answer to see the explanation.
Question 4 of 7Across a daylight-saving jump, why can adding
{ hours: 24 }differ from adding{ days: 1 }?Choose an answer to see the explanation.
Question 5 of 7What does this print?
Read the code, then predictconst timer = Temporal.Duration.from({ hours: 1, minutes: 30 }); console.log(timer.total({ unit: "minutes" }));Choose an answer to see the explanation.
Question 6 of 7Which statement about calendars is accurate?
Choose an answer to see the explanation.
Question 7 of 7How do you convert a legacy Date to a Temporal instant?
Choose an answer to see the explanation.
Key takeaways
- Temporal is a toolbox of focused types, not one do-everything Date object.
Instantis exact timeline time; plain types are calendar or clock values with no zone.ZonedDateTimecarries time-zone rules, so DST math can distinguish 24 hours from 1 calendar day.Durationrepresents lengths of time and can be totaled or rounded with explicit units.- Temporal values are immutable, and migration from Date starts cleanly with epoch milliseconds to
Instant.
Temporal one-liner.
Choose the Temporal type that matches your real-world time data, then let immutable, calendar-aware methods do the math.
Up next: Number & Math in depth.