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 typeTell apart an exact moment, a plain date, a plain clock time, and a time in a named place.
- 02Do date math safelyAdd, compare, and measure dates with values that never change, instead of changing a Date object.
- 03Move from Date to TemporalTurn an old Date's milliseconds number into a
Temporal.Instant, and know what to use instead of common Date code.
Temporal is a toolbox for time
INTERACTIVEJavaScript's old Date object tries to do every time job alone. One Date is used for all of these:
- an exact moment, like "when this error happened"
- a calendar date, like a birthday
- a clock time, like "the shop opens at 09:00"
- text for people to read
That makes mistakes easy. A Date can be changed by mistake. Its months start at 0. It reads some date text in surprising ways. And clock changes for summer time can break its math. The Date & time lesson shows each of these problems.
Temporal is the newer way to work with dates and times in JavaScript. It gives each job its own type. A type here is a kind of value, built for one job.
Temporal is new, and browsers are adding it one by one. Today these browsers have it:
- Chrome 144 and later, and Edge 144 and later
- Firefox 139 and later
- Safari only in its preview version, and Safari on iPhone and iPad not at all
When this page opens, it checks whether your browser has Temporal. If it does, the live cards run real Temporal code. If it doesn't, they show example output instead and say so. Our tests check that example output in Node.js, using its --harmony-temporal setting, which turns Temporal on.
const hasTemporal = typeof globalThis.Temporal !== "undefined";if (hasTemporal) { Temporal.Now.plainDateISO().toString();}Temporal in your browser: checking once the page loads
Sample: Not checked yet
The check runs once the page has loaded in your browser, so the page doesn't guess what your browser can do.
--harmony-temporal setting, which turns Temporal on.A pocket knife with many blades is handy, because one object does many jobs. But to build a cupboard, you want a real saw, a real hammer, and a real screwdriver. Each tool does one job well. Temporal gives you a separate tool for each time job.
- In real life: A stopwatch reading
- In JavaScript:
Temporal.Instant: one exact moment - In real life: A birthday on a paper calendar
- In JavaScript:
Temporal.PlainDate: a date with no clock time and no place - In real life: A clock face for a daily alarm
- In JavaScript:
Temporal.PlainTime: a clock time with no date - In real life: A flight leaving Mumbai at 09:00 local time
- In JavaScript:
Temporal.ZonedDateTime: a date, a clock time, and a place - In real life: A timer set for 90 minutes
- In JavaScript:
Temporal.Duration: a length of time
Try it now. Put each real situation with the Temporal type it needs. Read the explanation after each card.
- A time in a server log:
2024-03-10T06:30:00Z - A birthday: May 12, with no year needed for the reminder
- The shop opens every day at
09:00 - A meeting in Bengaluru at 10:00 on June 3
- A monthly bill for February 2024
- A countdown that lasts 1 hour 30 minutes
Put each situation with the Temporal type that fits it best. Each card explains why.
Temporal.Now & Instant
Temporal.Now gives you the current time. It has a few methods:
Temporal.Now.instant()gives the exact moment right now.Temporal.Now.plainDateISO()gives today's date.
The current time changes every second, so the examples in this lesson use fixed times instead. That way the output is always the same, and our tests can check it.
An Instant is one exact moment. It means the same moment everywhere on Earth. Use it for:
- lines in a server log
- times saved in a database
- the moment some saved data runs out
const instant = Temporal.Instant.from("2024-03-10T06:30:00Z");console.log(instant.toString());console.log(instant.epochMilliseconds);2024-03-10T06:30:00Z
1710052200000
Line 1 makes an Instant from date text. Line 2 prints it as text again. The Z at the end means UTC, the world's shared base time.
Line 3 prints epochMilliseconds. That is the number of milliseconds since the start of 1970 in UTC. It is the same kind of number that the old Date.now() gives. So an Instant is an easy way to move from old Date code to Temporal.
PlainDate, PlainTime & PlainDateTime
STEP THROUGHThree types start with the word "Plain". "Plain" means the value has no time zone. A time zone is a place's clock rules, like "New York time".
PlainDateis a date, like a birthday.PlainTimeis a clock time, like an opening time.PlainDateTimeis a date and a clock time together, like "January 31 at 09:30", before anyone says where.
Instant names one exact moment. The Z at the end means UTC, so everyone agrees which moment it is. A PlainDate is only a date, like a square on a paper calendar. It has no clock time and no place.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());Lines 1 to 3 make one value of each type. Lines 4 to 7 print them.
Months start at 1, like on a real calendar. Temporal.PlainDate.from("2024-01-31").month is 1 for January, not 0.
Temporal values are also immutable. That means they never change after they are made. Methods like add and with don't change the date you call them on. They make a new date and give it back.
add() never changes the date you call it on. It makes a new date and gives it back. So original still holds January 31, and the new date, February 29, goes into later.Step through how Temporal does date math without changing the original date. Change the number of months, then guess 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")));January has 31 days, but February doesn't. So what should "January 31 plus one month" be? Temporal lets you choose with the overflow option:
"constrain"(the default) moves the date back to the last real day of the month."reject"stops with an error instead.
const date = Temporal.PlainDate.from("2024-01-31");console.log(date.add({ months: 1 }).toString());try { console.log(date.add({ months: 1 }, { overflow: "reject" }).toString());} catch (error) { console.log(error.name);}2024-02-29
RangeError
Line 2 uses the default, so it prints the last day of February 2024. Line 4 asks for reject, so add throws an error. Line 6 prints the error's name, RangeError. Use reject when a day that doesn't exist should count as a mistake in the user's input.
const date = Temporal.PlainDate.from("2024-05-12");const appointment = date.with({ day: 20 });console.log(appointment.toString());console.log(date.toString());2024-05-20
2024-05-12
with makes a new date with only the part you name changed. Here it changes the day to 20. Line 4 prints the first date again, and it is still May 12.
const dates = ["2024-05-12", "2023-12-01", "2024-01-31"] .map((value) => Temporal.PlainDate.from(value));dates.sort((left, right) => Temporal.PlainDate.compare(left, right));console.log(dates.map((date) => date.toString()).join(", "));2023-12-01, 2024-01-31, 2024-05-12
Lines 1 and 2 turn three date texts into PlainDate values. Line 3 sorts them. Temporal.PlainDate.compare takes two dates. It gives -1 if the first is earlier, 0 if they are the same, and 1 if the first is later. Line 3 gives sort a small arrow function that calls compare on each pair of dates. Line 4 prints the dates in order.
Date texts like these happen to sort in the right order too. But compare says clearly that you are comparing dates.
February has no day 31. By default, Temporal picks the last real day of February instead. 2024 is a leap year, so February 2024 has 29 days. The answer is February 29.
PlainYearMonth & PlainMonthDay
Some dates are missing a part on purpose:
- A credit card's end date has a year and a month, but no day.
- A yearly birthday reminder has a month and a day, but no year.
Temporal has a type for each. So you don't have to make up a fake day or a fake year to fill 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());Line 1 makes February 2024. Line 2 makes February 29 with no year. The two dashes in "--02-29" stand where the year would be.
When you talk about a paper calendar, you don't always point at one square. Sometimes you mean the whole month page. Sometimes you mean a day that comes back every year.
- In real life: A whole page that says February 2024
- In JavaScript:
PlainYearMonth.from("2024-02") - In real life: The birthday square for Feb 29, any year
- In JavaScript:
PlainMonthDay.from("--02-29") - In real life: One square on one page: Feb 29, 2024
- In JavaScript:
PlainDate.from("2024-02-29")
ZonedDateTime & time zones
STEP THROUGHAn offset is how far a clock is from UTC, like -05:00 (five hours behind UTC). A time zone is more than one offset. It is a named set of rules, like America/New_York. The rules say:
- which offset the place uses on each day
- when the clocks move for summer time
- how those rules changed in past years
Many places move their clocks forward one hour in spring and back one hour in autumn. This is called daylight saving time, or DST.
A ZonedDateTime keeps three things together:
- one exact moment
- the date and clock time people see in that place
- the time zone's rules
A time zone is more than an offset. Step through to see why adding 24 hours and adding 1 day can give different clock times.
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, the clocks jumped from 01:59:59 straight to 03:00:00. India doesn't use DST today. But an Indian app with users abroad still has to handle clock changes like this one.
Because of the jump, adding { hours: 24 } and adding { days: 1 } give different clock times in the example:
{ hours: 24 }adds 24 real hours.{ days: 1 }moves to the same clock time on the next day.
In autumn, the clocks go back one hour. On November 3, 2024, New York saw 01:30 twice: once before the clocks went back, and once after. So "01:30 on November 3" could mean two different moments.
Disambiguation is how Temporal picks one. You tell it with the disambiguation option:
"earlier"picks the first 01:30."later"picks the second 01:30."reject"throws an error, so your app can ask the user to choose.
const localMeeting = "2024-11-03T01:30[America/New_York]";const earlier = Temporal.ZonedDateTime.from(localMeeting, { disambiguation: "earlier",});const later = Temporal.ZonedDateTime.from(localMeeting, { disambiguation: "later",});console.log(earlier.toString());console.log(later.toString());Line 1 is the meeting as text: the date, the clock time, and the time zone name in square brackets. This text format is a standard called RFC 9557. Lines 2 to 7 read the text twice, once with each choice:
- The first output picks the earlier 01:30, with offset
-04:00. - The second output picks the later 01:30, with offset
-05:00.
Both show 01:30 on the clock. But they are two different moments, one hour apart.
const date = Temporal.PlainDate.from("2024-05-12");const time = Temporal.PlainTime.from("09:30");const meeting = date.toPlainDateTime(time).toZonedDateTime("America/New_York");console.log(meeting.toString());console.log(meeting.toInstant().toString());2024-05-12T09:30:00-04:00[America/New_York]
2024-05-12T13:30:00Z
A form can ask for a date and a time without a place. Lines 1 and 2 hold those two answers. When the user picks a place, line 3 joins the date and time and adds the time zone with toZonedDateTime. Line 5 calls toInstant to get the exact moment, ready to save or send.
Duration & arithmetic
INTERACTIVEA Duration is a length of time, like "3 days" or "1 hour 30 minutes". It is not a point in time. You can get one in two ways:
- Measure between two values with
untilorsince. - Build one yourself with
Temporal.Duration.from.
You can also round a Duration, or turn it into one unit, like "90 minutes". You tell Temporal which unit you want.
until() measures the gap between them and gives back a Duration, a length of time. In 2024 the gap is 3 days: Feb 29, Mar 1, and Mar 2. P3D is how a Duration writes "3 days".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 and guess the four lines. Then run it.
until measures the gap between two dates and gives back a Duration. total turns a Duration into one unit.Two dates can give you a Duration. You can then turn a Duration into one unit, or round it.
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" }));An hour is always 60 minutes. But a month can have 28, 29, 30, or 31 days, and a year can have 365 or 366. So to round or total months and years, Temporal may need a start date. Then it knows which month or year you mean.
const duration = Temporal.Duration.from({ minutes: 95 });console.log(duration.round({ largestUnit: "hours", smallestUnit: "hours", roundingMode: "halfExpand",}).toString());const time = Temporal.PlainTime.from("09:40");console.log(time.round({ smallestUnit: "hour", roundingMode: "halfExpand" }).toString());PT2H
10:00:00
Lines 2 to 6 round 95 minutes to whole hours. halfExpand is the rounding you learned at school: halfway or more rounds up. So 95 minutes (1 hour 35 minutes) becomes 2 hours, printed as PT2H. Line 8 uses the same rule on the clock time 09:40, which becomes 10:00. This helps when you show times in one-hour slots.
const start = Temporal.PlainDate.from("2024-01-31");const end = Temporal.PlainDate.from("2024-03-31");console.log(start.until(end, { largestUnit: "days" }).toString());console.log(start.until(end, { largestUnit: "months" }).toString());console.log(end.since(start, { largestUnit: "months" }).toString());P60D
P2M
P2M
largestUnit sets the biggest unit the answer may use. The same two dates give two answers:
- With
"days", line 3 printsP60D: 60 days. - With
"months", line 4 printsP2M: 2 months.
Line 5 uses since, which measures from the other end, and also prints P2M. Pick the unit your app shows to its users.
Calendars
A calendar is a system for naming days: which year, which month, which day. Most of the world uses the Gregorian calendar. Others include the Hebrew and Japanese calendars.
Temporal uses the ISO calendar by default. That is the Gregorian calendar, written the way computers usually write it, like 2024-05-01. But a Temporal date can also use another calendar. The day stays the same; only its name changes.
const isoDate = Temporal.PlainDate.from("2024-05-01");const japaneseDate = isoDate.withCalendar("japanese");console.log(japaneseDate.toString());2024-05-01[u-ca=japanese]
Line 2 switches the date to the Japanese calendar. Line 3 prints it. The text in square brackets, [u-ca=japanese], is a note that says which calendar the date uses.
Japanese era names, and how dates look in other languages, depend on data inside each browser. So this lesson only shows the output our tests check: the date with its calendar note.
A calendar and a time zone answer different questions:
- A calendar answers: "Which year, month, and day is this?"
- A time zone answers: "What does the clock show in this place at this moment?"
Switching to the Japanese calendar doesn't move the moment, and it doesn't change the time zone.
Migrating from Date
INTERACTIVETo move old Date code to Temporal, first ask: "What kind of time do I really have?"
- An exact moment? Turn it into an
Instant. - A date from a form? Read it as a
PlainDate. - A flight or a meeting in a place? Use a type with a time zone,
ZonedDateTime.
In the table, each row is one common Date habit. Read across to see what to use instead, and why.
| When you used to write | Write this instead | Why it's clearer |
|---|---|---|
Date.now() | Temporal.Now.instant() | You get an exact moment with a name, not just a number. |
new Date(y, m, d) | Temporal.PlainDate.from({ year, month, day }) | Months start at 1, and the type says there is no time zone. |
date.toISOString() | instant.toString() | An Instant already prints as standard date text ending in Z. |
date.setDate(date.getDate() + 1) | plainDate.add({ days: 1 }) | It makes a new date. The old one stays the same. |
date.getTime() | instant.epochMilliseconds | The milliseconds number is a named property of the 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 holds one exact moment. Turn that moment into an Instant first. Change it into a date or a time in a place later, on purpose.
const dueDate = Temporal.PlainDate.from("2024-05-12");console.log(JSON.stringify(dueDate));console.log(dueDate.toString({ calendarName: "never" }));Before you save or send a value, you turn it into text. Line 2 does this with JSON.stringify and prints "2024-05-12", a date with no time added. Line 3 gets the same date text with toString. The option { calendarName: "never" } says: don't add a calendar note.
Save the value just as it is. Add a time zone later, only when you really have a place and a clock time.
A practical scheduling decision
When you build a form or a database field, don't start from "Which date method do I know?". Start from "What did the user actually type in?"
- A birthday reminder needs a month and a day that come back every year.
- A card's end date needs a year and a month.
- A meeting invitation needs a clock time and a named time zone.
- A server log needs one exact moment.
In the table, find what your app receives in the first column. The second column is the type to use.
| What your app receives | Type to use | Why |
|---|---|---|
A log line ending in Z | Instant | It already names one exact moment. |
| A birthday form with a month and a day | PlainMonthDay | The reminder comes back every year. |
A card end date such as 2028-04 | PlainYearMonth | There is no day on purpose. |
| A meeting at 09:00 in a named time zone | ZonedDateTime | The clock time and the place's rules both matter. |
Keeping the types apart also helps when you save them:
- Save an
Instantas date text or as its milliseconds number. - Save a
PlainDateas date-only text. - Save a future meeting with its time zone name next to it.
Don't turn every value into "midnight UTC" just to fit one old database field.
const date = Temporal.PlainDate.from("2024-05-12");const time = Temporal.PlainTime.from("09:30");console.log(date.toLocaleString("en-GB"));console.log(time.toLocaleString("en-GB"));12/05/2024
09:30:00
A locale is a language and region code, like "en-GB" for British English. It decides how dates look, for example day first or month first. Lines 3 and 4 pass "en-GB", so the date shows day first.
toLocaleString makes text for people to read. Don't save that text. Keep the Temporal value, and turn it into display text again whenever you need it.
Common misconceptions
“Temporal works in every browser now.”
Not yet. Check for it first. This lesson never loads extra code to add Temporal, and it labels example output when your browser doesn't have it.
“A PlainDate is a date at midnight.”
No. A PlainDate has no clock time and no time zone. "Midnight" would need a place: midnight where?
“A time zone is just an offset.”
An offset is one number, right for one moment. A named time zone has rules, and its offset can change during the year.
“add({ days: 1 }) and add({ hours: 24 }) always give the same result.”
Not on a ZonedDateTime on a day when the clocks change. The New York example shows them landing at different clock times.
“Temporal values change, like Date does.”
No. Temporal methods give back new values. The original stays the same, unless you put a new value in its variable yourself.
“Two equal dates are equal with ===.”
Two dates made separately are two separate objects, so === says false. Use equals to compare the dates themselves. And keep the result of add in a variable.
const first = Temporal.PlainDate.from("2024-05-12");const second = Temporal.PlainDate.from("2024-05-12");console.log(first === second);console.log(first.equals(second));const due = first.add({ days: 1 });console.log(first.toString());console.log(due.toString());false
true
2024-05-12
2024-05-13
Line 3 asks, "Are these the same object?" They aren't, so === prints false. Line 4 asks, "Are these the same date?" They are, so equals prints true. Line 5 keeps the new date from add in due. Line 6 shows that first didn't change. Line 7 prints the new date.
Some Temporal types sound alike. Each row of the table is one type: what it holds, and when to use it.
| Type | What it holds | When to use it |
|---|---|---|
Instant | One exact moment. | Logs, the moment something runs out, and times from old Date code. |
PlainDate | A year, a month, a day, and a calendar. | Birthdays, due dates, and dates typed into forms. |
PlainTime | An hour, a minute, and a second. | Daily alarms and opening times. |
ZonedDateTime | An exact moment, plus the clock time and time zone of a place. | Flights, meetings, and deadlines in a place. |
Duration | A length of time. | Countdowns, gaps between dates, and date math. |
Practice: Temporal
5 EXERCISESRead the code. What does the first line of output say?
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 }) makes a new date: 2024-02-29. February has no day 31, so Temporal picks its last day. The original date stays 2024-01-31.
Which Temporal type fits a server log time such as 2024-03-10T06:30:00Z?
Use Temporal.Instant for a log time. Everyone must agree on the exact moment the log line records.
How many days are there between the two dates?
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);2024 is a leap year. From Feb 28 to Mar 2 is 3 days: Feb 29, Mar 1, then Mar 2.
What Instant text does this code print first?
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 moment 2024-03-10T06:30:00.000Z. Its milliseconds number makes the Instant 2024-03-10T06:30:00Z.
Rewrite the code with Temporal so the start date doesn't change. 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());add makes a new PlainDate, so start stays 2024-01-31 and due becomes 2024-02-29.
Quiz: check your understanding
7 QUESTIONSFor each question, first ask: "What kind of time is this: an exact moment, a plain date or time, a time in a place, or a length of time?" Each answer explains why it is right or wrong.
Question 1 of 7Which Temporal type means one exact moment, the same everywhere on Earth?
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 fits the end date on a credit card, like 04/28?
Choose an answer to see the explanation.
Question 4 of 7On a day when the clocks change, why can adding
{ hours: 24 }give a different result 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 sentence about calendars is true?
Choose an answer to see the explanation.
Question 7 of 7How do you turn an old Date into a Temporal Instant?
Choose an answer to see the explanation.
Key takeaways
- Temporal is a toolbox: one type for each time job, instead of one
Dateobject for everything. - An
Instantis one exact moment. The Plain types are dates or clock times with no time zone. - A
ZonedDateTimeknows its place's clock rules, so on a clock-change day, 24 hours and 1 day can be different. - A
Durationis a length of time. You can round it, or turn it into one unit like minutes. - Temporal values never change. To move from
Date, start by turning its milliseconds number into anInstant.
Remember the one-liner.
Pick the Temporal type that matches your time data. Then let its methods do the date math, without changing the original.
Up next: Number & Math in depth.