Matching Temporal Types to Real Date Tasks: Seven Worked Examples
Work through seven everyday date and time tasks with JavaScript Temporal, from plain dates and month overflow to time zones, DST arithmetic and validation.
This article is published in English.
Sources: JavaScript Temporal: 7 Date and Time Problems Explained Simply
Date requirements usually sound trivial: extend a subscription by a month, show a meeting in a colleague's time zone, count the days between two dates. The complications arrive later, when February turns out to be short, a daylight-saving change makes "tomorrow" 23 hours away, or a birthday has to be stored even though it has no time zone at all. JavaScript's Temporal API addresses this by giving each kind of value its own type, so the type itself documents what a value means. The seven worked examples below show which type fits which job, where the defaults may surprise you, and which decisions Temporal deliberately leaves to your application.
Setting up a place to run the examples
Native support is still rolling out. At the time of writing, MDN lists Temporal as having limited availability, so confirm support in your target browsers and Node.js version before relying on the built-in implementation. MDN's Temporal reference page tracks the current compatibility status.
To experiment without native support, install the Temporal polyfill into an empty project folder. You need Node.js and npm:
mkdir temporal-demo
cd temporal-demo
npm init -y
npm install @js-temporal/polyfill@0.5.1
Create a file named demo.mjs and start it with this import:
import { Temporal } from "@js-temporal/polyfill";
Add any of the examples below the import and run the file:
node demo.mjs
All examples assume the same import. The polyfill exports a local Temporal binding and does not install a global, so each file that uses it needs the import. The version is pinned to 0.5.1 because that is the release the examples were written against; check the polyfill's releases before using it in a real project.
1. Store a calendar date without a time or zone
A birthday or a contract deadline often has no meaningful time of day. If the requirement is simply "October 15", attaching midnight and a time zone invents information, and that invented information is exactly what later causes the date to shift by a day when it is displayed elsewhere. Temporal.PlainDate represents just the date:
const deadline = Temporal.PlainDate.from("2026-10-15");
Its fields are plain numbers and it serializes back to an ISO string:
console.log(deadline.year); // 2026
console.log(deadline.month); // 10
console.log(deadline.day); // 15
console.log(deadline.toString()); // 2026-10-15
The value has no hour, no UTC offset and no zone. It uses the ISO calendar by default, and, unlike the legacy Date object, months are numbered 1 through 12 rather than starting at zero.
Keep the limitation in mind: a PlainDate does not pin down any moment in absolute time. If the deadline really means "5 PM in Delhi", you also need a time and a zone, which is what ZonedDateTime is for.
2. Derive new dates without mutating the original
Shared mutable dates are a classic source of bugs: one part of the code adjusts a value that another part is still reading. Temporal objects are immutable, and arithmetic always produces a new object:
const originalDate = Temporal.PlainDate.from("2026-10-15");
const extendedDate = originalDate.add({ days: 7 });
Logging both values shows that the original is untouched:
console.log(originalDate.toString()); // 2026-10-15
console.log(extendedDate.toString()); // 2026-10-22
This means you can keep the previous deadline and the extended one side by side without cloning anything first.
The flip side is a common mistake: calling originalDate.add({ days: 7 }) on its own line and expecting originalDate to change. It does not. Assign or return the result.
3. Decide what "one month later" means
Adding a month to January 31 has no single correct answer, because February 31 does not exist. Your application needs a rule. Temporal's default is to constrain the result to the nearest valid date. Start from the end of January:
const januaryEnd = Temporal.PlainDate.from("2026-01-31");
Adding one month clamps the day to the end of February:
const nextMonth = januaryEnd.add({ months: 1 });console.log(nextMonth.toString()); // 2026-02-28
When silently moving the day is unacceptable, ask Temporal to throw instead by passing overflow: "reject". Using a billing date as the example:
const billingDate = Temporal.PlainDate.from("2026-01-31");
The rejection surfaces as a RangeError, which the snippet catches while rethrowing anything else:
try {
billingDate.add({ months: 1 }, { overflow: "reject" });
} catch (error) {
if (!(error instanceof RangeError)) throw error;
console.log("That day does not exist in the next month.");
}
Neither option is your billing policy; each is only a mechanism for implementing one. There is also a compounding effect to watch. Once January 31 has been clamped to February 28, adding another month to that result produces March 28, not March 31, so a schedule built by repeatedly adding one month to the previous value drifts. If the rule is "the last day of each month", model that rule explicitly, for instance by always computing from the original anchor date or by using the month's length, instead of chaining additions.
4. Display one instant in several time zones
A timestamp for an event identifies one exact moment. The local date and time on someone's screen depend on where they are. Temporal.Instant represents the moment itself:
const meeting = Temporal.Instant.from("2026-10-15T09:00:00Z");
Converting it into named zones gives the local wall-clock time in each place, and converting back shows that both still refer to the same instant:
const delhi = meeting.toZonedDateTimeISO("Asia/Kolkata");
const london = meeting.toZonedDateTimeISO("Europe/London");console.log(delhi.toString());
// 2026-10-15T14:30:00+05:30[Asia/Kolkata]console.log(london.toString());
// 2026-10-15T10:00:00+01:00[Europe/London]console.log(delhi.toInstant().equals(london.toInstant()));
// true
The local representations differ, yet the underlying instant is identical. (The run-together lines in this snippet should each be a separate statement.)
Use named zones such as Europe/London rather than fixed offsets. A named zone carries the rules for how its offset changes over the year; a hard-coded +01:00 only describes London during summer time.
This is where many bugs start: an offset is not a time zone. An offset states the difference from UTC at one particular moment, while a named zone supplies the rules that determine that difference for any moment.
The strings above are machine-oriented, including the bracketed zone annotation. For anything shown to users, format the value for their locale, for example with toLocaleString() or Intl.DateTimeFormat, instead of printing the raw string.
5. Keep "tomorrow" and "24 hours later" apart
Most of the year these phrases give the same answer. Around a daylight-saving transition they do not. Take noon in New York on March 7, 2026, the day before clocks move forward:
const beforeClockChange = Temporal.ZonedDateTime.from(
"2026-03-07T12:00:00-05:00[America/New_York]"
);
Adding one calendar day and adding 24 hours produce different local times:
const tomorrow = beforeClockChange.add({ days: 1 });
const after24Hours = beforeClockChange.add({ hours: 24 });console.log(tomorrow.toString());
// 2026-03-08T12:00:00-04:00[America/New_York]console.log(after24Hours.toString());
// 2026-03-08T13:00:00-04:00[America/New_York]
Calendar arithmetic keeps the wall-clock time at noon. Elapsed-time arithmetic counts exactly 24 hours, and because the clock jumped forward an hour overnight, it lands at 1 PM.
Each is right for different requirements. A daily appointment usually means "same local time tomorrow", so add days. A token or reservation that expires after exactly 24 hours should add hours. Temporal can compute either; you have to decide which meaning your feature needs, and it is worth writing that decision down.
6. Reject invalid dates and nonexistent local times
When validating form input, an impossible date should produce an error, not quietly become a different date. For object input, request strict handling with overflow: "reject":
try {
Temporal.PlainDate.from(
{ year: 2026, month: 2, day: 30 },
{ overflow: "reject" }
);
} catch (error) {
if (!(error instanceof RangeError)) throw error;
console.log("Please choose a valid calendar date.");
}
Without that option, the object { year: 2026, month: 2, day: 30 } would be constrained to February 28. ISO strings behave differently: parsing "2026-02-30" is rejected outright rather than repaired. Do not assume that string parsing and object construction share the same overflow behavior.
A valid date can still combine with a local time that never occurs. When New York moves its clocks forward on March 8, 2026, 2:30 AM is skipped entirely. The disambiguation option controls what happens with such input:
try {
Temporal.ZonedDateTime.from(
"2026-03-08T02:30[America/New_York]",
{ disambiguation: "reject" }
);
} catch (error) {
if (!(error instanceof RangeError)) throw error;
console.log("Choose a time that exists and is unambiguous.");
}
With disambiguation: "reject", Temporal throws for both skipped times and repeated times, the hour that occurs twice when clocks fall back. The other settings select an interpretation automatically. For something like a booking form, asking the user to pick a valid time is usually clearer than shifting their appointment without telling them.
7. Compare, measure and sort dates explicitly
For chronological comparisons, use the type's own methods. Start with two dates:
const startDate = Temporal.PlainDate.from("2026-10-15");
const endDate = Temporal.PlainDate.from("2026-10-20");
Temporal.PlainDate.compare() returns -1, 0 or 1 when the first argument is earlier, equal or later. until() returns a Temporal.Duration, which prints in ISO 8601 duration format, so five days appears as P5D:
console.log(Temporal.PlainDate.compare(startDate, endDate));
// -1const difference = startDate.until(endDate, {
largestUnit: "days",
});console.log(difference.days); // 5
console.log(difference.toString()); // P5D
For PlainDate, the difference is measured in calendar days. It does not compute elapsed hours in any time zone, and it does not decide whether your product counts both the start and the end day; that off-by-one rule is yours to define.
Because compare has the signature that Array.prototype.sort expects, it can be passed straight in. Given an unsorted list of dates:
const dates = [
"2026-11-03",
"2026-10-15",
"2026-10-20",
].map(value => Temporal.PlainDate.from(value));
sorting becomes a one-liner:
dates.sort(Temporal.PlainDate.compare);console.log(dates.map(date => date.toString()).join(", "));
// 2026-10-15, 2026-10-20, 2026-11-03
Avoid habits carried over from Date. Subtracting Temporal objects or comparing them with < and > throws for these types, which is deliberate: it prevents accidental comparisons of the wrong kind. And === checks object identity, so two equal dates created separately are not ===; use .equals() for value equality. For more subtle ways old habits leak back in, see our guide to Temporal API traps that reintroduce date bugs.
Choose the type before writing the calculation
Most date bugs are really modeling bugs, so settle what a value means first:
- A date with no time or zone:
PlainDate - A clock time with no date or zone:
PlainTime - A local date and time with no zone:
PlainDateTime - An exact point on the global timeline:
Instant - A date and time tied to a named zone:
ZonedDateTime - An amount of calendar or clock time:
Duration
This choice deserves a conversation when you design an API contract or a database column. A field simply called date invites disagreement about whether it holds a calendar date, a UTC timestamp or a local time.
Migration does not have to be all at once. Pick one feature with clear requirements, define its input format, Temporal type, time-zone policy and expected output, and add test cases for month ends and clock changes where they apply. Working date code can be left alone until you have a reason to touch it.
Pre-production checklist
- Verify Temporal support in every environment you ship to, and if you use the polyfill, account for its download and parse cost in your bundle budget.
- Agree on serialization with your backend. A plain date, a UTC instant and a string with a bracketed zone identifier are three different formats.
- Make business rules explicit: may "one month later" adjust the day, should ambiguous appointment times be rejected, and does a duration mean calendar days or elapsed hours?
Wrapping up
Temporal's main contribution is not a nicer API surface but forcing distinctions that Date blurred: dates versus instants, offsets versus zones, calendar arithmetic versus elapsed time. It gives you precise tools for each case, including strict modes that fail loudly instead of guessing. What it cannot do is choose your policies. Decide those rules deliberately, encode them in the type and options you pass, and cover the edge cases with tests.