What does a Temporal.Duration represent, and why does calling total({ unit: 'day' }) on a duration of one month throw unless you pass relativeTo?
answer
- how much, never when
- fields kept as given
- some units have no fixed length
- the anchor changes the answer
- one unit sits in both families
basics
~20 sA Temporal.Duration is a length of time held as a bag of unit fields, anchored to no point on the calendar. Years, months and weeks have no fixed length, so converting them to days needs a starting point, supplied as relativeTo — otherwise it throws a RangeError.
solid answer
~50 sA `Temporal.Duration` is a quantity of time — a bag of fields from years down to nanoseconds, plus a sign — and crucially it is not attached to any moment. It also does not self-normalise: `Temporal.Duration.from({ minutes: 90 }).toString()` is `'PT90M'`, not `'PT1H30M'`, until you round it. That matters because exact-time units convert freely (`total({ unit: 'hour' })` on 90 minutes is `1.5`), but calendar units do not: a month is 28 to 31 days and a year is 365 or 366, so "how many days is one month" has no answer without knowing which month. Temporal refuses to guess and throws a `RangeError` unless you pass `relativeTo` — a `PlainDate` or `ZonedDateTime` to measure from. The same applies to `round()` and `Temporal.Duration.compare()` on calendar-unit durations. Days are the subtle case: with no `relativeTo` a day counts as 24 hours, but with a `ZonedDateTime` anchor they respect DST and can be 23 or 25.
code
javascript · 16 linesimport { Temporal } from '@js-temporal/polyfill';
const d = Temporal.Duration.from({ minutes: 90 });
console.log(d.toString()); // PT90M — no auto-normalising
console.log(d.total({ unit: 'hour' })); // 1.5
console.log(d.round({ largestUnit: 'hour' }).toString()); // PT1H30M
const month = Temporal.Duration.from({ months: 1 });
try {
month.total({ unit: 'day' });
} catch (e) {
console.log(e.constructor.name); // RangeError — one month from when?
}
console.log(month.total({ unit: 'day', relativeTo: Temporal.PlainDate.from('2026-02-01') })); // 28
console.log(month.total({ unit: 'day', relativeTo: Temporal.PlainDate.from('2026-03-01') })); // 31go deeper
Know that a Duration is an amount of time with no date attached, and that months and years cannot be turned into days without saying which month or year you mean.
Explain why exact-time units convert freely while calendar units require relativeTo, what largestUnit does to the shape of a difference, and why durations deliberately do not self-normalise.
Show judgment about representation: when to persist an ISO duration string rather than a millisecond count, when a ZonedDateTime anchor is needed so days respect DST, and what a naive thirty-day month does to billing or reporting numbers.
Own the convention for how intervals cross service and storage boundaries — which units are permitted in stored durations, where the anchor comes from, and why an averaged month is a defect rather than a convenience in financial or contractual logic.
## A duration is not a date `Temporal.Duration` answers "how much time", never "when". It holds `years`, `months`, `weeks`, `days`, `hours`, `minutes`, `seconds`, `milliseconds`, `microseconds` and `nanoseconds`, plus a sign — a duration is entirely positive or entirely negative, exposed as `duration.sign`. You construct one directly, or get one back from a difference: ```js const a = Temporal.PlainDate.from('2026-01-01'); const b = Temporal.PlainDate.from('2026-03-15'); a.until(b, { largestUnit: 'month' }).toString(); // 'P2M14D' a.until(b, { largestUnit: 'day' }).toString(); // 'P73D' ``` Both results describe the same interval. `largestUnit` decides the *shape* of the answer, and that already hints at the core issue: the same gap is two months and fourteen days, or seventy-three days, and neither form can be converted into the other without knowing where you started. ## Durations do not normalise themselves A duration keeps the fields you gave it: ```js const d = Temporal.Duration.from({ minutes: 90 }); d.toString(); // 'PT90M' — not 'PT1H30M' d.minutes; // 90 d.hours; // 0 ``` This is intentional. "90 minutes" and "1 hour 30 minutes" may be the same length but they are different statements, and a library that silently rewrote one into the other would destroy information a UI might want. Rebalancing is explicit: ```js d.round({ largestUnit: 'hour' }).toString(); // 'PT1H30M' d.total({ unit: 'hour' }); // 1.5 ``` `round()` returns a new Duration in a different shape; `total()` returns a plain Number of the requested unit, including a fraction. ## Why calendar units need an anchor Exact-time units have fixed ratios: an hour is always 60 minutes, a minute always 60 seconds. Calendar units do not. A month is 28, 29, 30 or 31 days. A year is 365 or 366. A week is 7 days, but converting weeks to months again requires knowing the months. So: ```js Temporal.Duration.from({ months: 1 }).total({ unit: 'day' }); // RangeError — one month from when? Temporal.Duration.from({ months: 1 }).total({ unit: 'day', relativeTo: Temporal.PlainDate.from('2026-02-01'), }); // 28 Temporal.Duration.from({ months: 1 }).total({ unit: 'day', relativeTo: Temporal.PlainDate.from('2026-03-01'), }); // 31 ``` The two answers differ by three days from the same duration, which is the whole argument for making the anchor mandatory. A library that returned `30` here — an average month — would be quietly wrong in every individual case. The same rule governs `round()` when the duration contains or targets calendar units, and `Temporal.Duration.compare(a, b, { relativeTo })`, because ordering "one month" against "30 days" also depends on which month. ## The days edge case `days` sits between the two families. Without `relativeTo`, Temporal treats a day as exactly 24 hours, so `Temporal.Duration.from({ days: 1 }).total({ unit: 'hour' })` is `24`. That is the right default for a value with no location attached. But if you pass a `ZonedDateTime` as `relativeTo`, days become real calendar days in that zone, and a day spanning a DST transition is 23 or 25 hours. So the *same* duration and the *same* call can produce 23, 24 or 25 depending on the anchor you supply. Passing a `PlainDate` anchor keeps days at 24 hours, because a plain date has no zone and therefore no transitions. Knowing which anchor type you handed in is part of knowing what the number means. ## Practical use Durations are what you feed to arithmetic — `zdt.add(duration)` — and what you get back from `until()` and `since()`. A few habits follow: - Store durations as ISO 8601 strings (`'P2M14D'`, `'PT90M'`) via `toString()`/`toJSON()`, and parse with `Temporal.Duration.from()`. Storing a raw millisecond count silently converts a calendar statement into an exact-time one. - Choose `largestUnit` on `until()` to match how the result will be read: `'day'` for a countdown, `'month'` for an age or a subscription term. - Reach for `total()` when you need a number for maths or a chart, and `round()` when you need a differently shaped duration to display. - If your duration only ever contains hours and smaller, none of the anchoring rules apply and you can convert freely. The interview point is not the method names. It is recognising that "one month" is not a length until you say when, and that Temporal turning that into a thrown `RangeError` — instead of a plausible average — is the feature.
- Why does Temporal.Duration.from({ minutes: 90 }) keep 90 minutes instead of balancing to one hour thirty?Because the two are different statements even though they are the same length, and silently rewriting one loses information a caller may need for display. Rebalancing is explicit through `round({ largestUnit: 'hour' })`, which returns a new duration, or `total({ unit: 'hour' })`, which returns the number 1.5.
- How does the result of total({ unit: 'hour' }) on a one-day duration change with the relativeTo you pass?With no anchor, a day is defined as 24 hours. With a `PlainDate` anchor it is still 24, since a plain date has no zone. With a `ZonedDateTime` anchor it becomes a real calendar day in that zone, so a day containing a DST transition totals 23 or 25 hours. The anchor type, not just the duration, determines the number.
- What does largestUnit control on PlainDate.prototype.until()?The shape of the returned duration. `{ largestUnit: 'day' }` between 2026-01-01 and 2026-03-15 gives `P73D`; `{ largestUnit: 'month' }` gives `P2M14D`. Same interval, different decomposition. Pick the one matching how the value will be read, because converting between the shapes afterwards needs the anchor again.
saying these in an interview costs you the question
- A month is 30 days, so the conversion should just work
- Durations normalise themselves into the largest sensible units
- A day in a duration is always exactly 24 hours
- Durations remember the dates they were computed from
- Storing a duration as milliseconds is always equivalent