Modern JS Temporal API — Date, Time & Math
Master wall-clock calendar primitives (PlainDate, PlainTime, PlainDateTime), handle ISO durations with Temporal.Duration, perform pure mathematical arithmetic with .add() and .subtract(), measure exact intervals using .since() and .until(), and leverage the complete legacy-to-Temporal migration guide.
Temporal Arithmetic & Duration Pipeline
Unlike legacy JavaScript where date math relies on messy millisecond conversions, Temporal performs calendar-aware arithmetic that handles leap years, month length variations, and DST changes natively.
1. Wall-Clock Calendar Primitives
Real-world applications frequently handle dates that have no meaningful timezone or clock hour attached (like birth dates, wedding anniversaries, or invoice due dates). Temporal provides dedicated primitive objects:
Temporal.PlainDate
Temporal.PlainTime
Temporal.PlainDateTime
Instantiating Plain Primitives
// 1. PlainDate creation (1-indexed month!):
const birthday = Temporal.PlainDate.from('1998-04-12');
console.log(`Born on day ${birthday.day}, month ${birthday.month} (${birthday.dayOfWeek})`);
// 2. PlainTime creation:
const storeOpens = Temporal.PlainTime.from({ hour: 8, minute: 30 });
console.log('Store opens at:', storeOpens.toString()); // "08:30:00"
// 3. Merging Date and Time into a PlainDateTime:
const examDateTime = birthday.toPlainDateTime(storeOpens);
console.log('Exam scheduled:', examDateTime.toString());
Try it in Playground
2. Temporal.Duration & Pure Date Arithmetic
A Duration represents a physical length of time (e.g. 3 weeks, 2 days, and 45 minutes). Temporal provides robust math without millisecond calculations:
Adding & Subtracting Durations
const startDate = Temporal.PlainDate.from('2026-01-15');
// Adding 45 days and 2 months:
const futureDate = startDate.add({ months: 2, days: 45 });
console.log('Future Date:', futureDate.toString());
// Subtracting 1 week:
const oneWeekAgo = startDate.subtract({ weeks: 1 });
console.log('One week prior:', oneWeekAgo.toString());
// Leap-Year and month overflow handling:
const endOfJan = Temporal.PlainDate.from('2026-01-31');
const nextMonth = endOfJan.add({ months: 1 });
// Safely clamps to Feb 28 (2026 is not a leap year)!
console.log('Safe clamped date:', nextMonth.toString()); // 2026-02-28
Try it in Playground
Measuring Intervals: .since() and .until()
To compute the exact elapsed duration between two dates, use date1.until(date2) or date2.since(date1):
Calculating Elapsed Time Between Dates
const today = Temporal.PlainDate.from('2026-05-15');
const launch = Temporal.PlainDate.from('2026-12-25');
// How much time until launch?
const countdown = today.until(launch, { largestUnit: 'month' });
console.log(`Months: ${countdown.months}, Days: ${countdown.days}`);
// Output: Months: 7, Days: 10
// As total days:
const totalDays = today.until(launch, { largestUnit: 'day' }).days;
console.log(`Total days remaining: ${totalDays}`); // 224 days
Try it in Playground
Interactive Mini-Lab: Temporal Math & Interval Engine
Select a starting date, apply a duration shift, and watch the calendar engine calculate safe, non-overflowing results in real time:
Start Date & Operations
Computed Temporal Output
3. Complete Legacy Date → Temporal Migration Guide
Here is the definitive reference for refactoring legacy Date patterns into modern Temporal in existing production projects:
| Goal | Legacy Date (Deprecated / Bug-Prone) |
Modern Temporal (Recommended) |
|---|---|---|
| Current Moment | new Date() |
Temporal.Now.instant() |
| Current Date Only | new Date() (stripping hours manually) |
Temporal.Now.plainDateISO() |
| Add 7 Days | d.setDate(d.getDate() + 7) (Mutates in place!) |
date.add({ days: 7 }) (Pure immutable copy) |
| Time Difference | (d2.getTime() - d1.getTime()) / 86400000 |
d1.until(d2, { largestUnit: 'day' }).days |
| Month Property | d.getMonth() (0 = January! 11 = December!) |
d.month (1 = January, 12 = December) |
| Format in Timezone | d.toLocaleString(..., { timeZone: 'Asia/Tokyo' }) |
instant.toZonedDateTimeISO('Asia/Tokyo').toLocaleString() |
Hands-on Challenge: SaaS Subscription Renewal & Grace Period Engine
Coding ChallengeYou are implementing the billing subscription logic for an enterprise SaaS platform. Given an initial signup date, calculate renewal deadlines, leap-safe anniversary dates, and whether an account is currently in its 14-day grace period:
- Accept an initial signup date string (e.g.
"2024-02-29"leap day). - Calculate the Next Annual Renewal Date using
.add({ years: 1 }). - Calculate the Grace Period Expiration by adding 14 days to the renewal date.
- Calculate total remaining days until expiration from a given check date using
.until().
/**
* Calculates SaaS renewal and grace period deadlines with Temporal precision.
*/
function calculateSubscriptionStatus(signupDateStr, checkDateStr) {
const signupDate = Temporal.PlainDate.from(signupDateStr);
const checkDate = Temporal.PlainDate.from(checkDateStr);
// 1. Calculate next annual renewal (Handles leap year 2024-02-29 -> 2025-02-28 safely)
const renewalDate = signupDate.add({ years: 1 });
// 2. Grace period ends 14 days after renewal date
const gracePeriodEnd = renewalDate.add({ days: 14 });
// 3. Days until expiration from today
const daysUntilGraceEnd = checkDate.until(gracePeriodEnd, { largestUnit: 'day' }).days;
const isExpired = Temporal.PlainDate.compare(checkDate, gracePeriodEnd) > 0;
const inGracePeriod = Temporal.PlainDate.compare(checkDate, renewalDate) >= 0 && !isExpired;
return {
signupDate: signupDate.toString(),
renewalDate: renewalDate.toString(),
gracePeriodEnd: gracePeriodEnd.toString(),
daysRemaining: Math.max(0, daysUntilGraceEnd),
inGracePeriod,
isExpired
};
}
// Verification on Leap Day Signup:
const result = calculateSubscriptionStatus('2024-02-29', '2025-03-05');
console.log(result);
// Output shows safe renewal on 2025-02-28 and grace period expiring 2025-03-14!
Date, adding 1 year to February 29th notoriously spilled into March 1st. Temporal handles leap day clamping cleanly and deterministically.
Chapter 20 Knowledge Check
Validate your mastery of Temporal arithmetic, durations, and plain calendar primitives.
Temporal.Instant
Temporal.PlainDate
Temporal.ZonedDateTime
Temporal.PlainTime
PlainDate.from('2026-01-31').add({ months: 1 }))?Date).
RangeError exception.
NaN.
startDate.delta(endDate)
startDate.until(endDate)
startDate.minus(endDate)
Temporal.diff(startDate, endDate)
Temporal.PlainDate objects in ascending chronological order?dates.toSorted(Temporal.PlainDate.compare)
dates.sort((a, b) => a - b)
dates.sortByDate()
Temporal.sort(dates)
Temporal.Interval
Temporal.TimeSpan
Temporal.Duration
Temporal.Period