Add and Subtract Durations with Temporal
To add or subtract two durations, call .add() or .subtract() on a Temporal.Duration; when either operand contains years, months, or weeks you must pass a relativeTo reference so the result can be balanced. Part of Temporal.Duration Arithmetic.
Why This Scenario Is Tricky
Adding PT40M to PT50M is unambiguous — minutes and hours are fixed-length, so the answer is always PT1H30M. But adding P1M (one month) to P10D (ten days) has no fixed answer: the result in days depends entirely on which month you started in, because February, March, and April have different lengths. Temporal.Duration does not carry a start date, so it physically cannot balance a calendar-unit sum on its own.
That is why .add() and .subtract() accept a relativeTo option. Without it, any operand containing years, months, or weeks throws a RangeError. With it, Temporal walks the calendar from that anchor and produces a correctly balanced duration. The same arithmetic anchored to two different months produces two different day counts — which surprises people who expect duration math to be a pure function of its inputs. It is, but the inputs include the anchor.
Minimal Working Solution
import { Temporal } from '@js-temporal/polyfill';
const a = Temporal.Duration.from({ hours: 1, minutes: 40 });
const b = Temporal.Duration.from({ minutes: 50 });
// Fixed units only → no relativeTo needed; 100 minutes balances to 1h40m... + 50m = 2h30m
const sum = a.add(b);
console.log(sum.toString()); // 'PT2H30M'
// Subtract works the same way
const diff = a.subtract(b);
console.log(diff.toString()); // 'PT50M'
For calendar units, supply an anchor:
import { Temporal } from '@js-temporal/polyfill';
const oneMonth = Temporal.Duration.from({ months: 1 });
const tenDays = Temporal.Duration.from({ days: 10 });
// relativeTo anchors the months so the result can be balanced into days
const total = oneMonth.add(tenDays, { relativeTo: '2026-02-01' });
console.log(total.toString()); // 'P1M10D' (kept as 1 month + 10 days, balanced from Feb 1)
Full Production Version
A typed helper that validates the operands, decides whether an anchor is required, and balances the result up to a chosen largestUnit.
import { Temporal } from '@js-temporal/polyfill';
type DurationInput = Temporal.Duration | string | Temporal.DurationLike;
type RelativeRef =
| Temporal.PlainDate
| Temporal.PlainDateTime
| Temporal.ZonedDateTime
| string;
const CALENDAR_FIELDS = ['years', 'months', 'weeks'] as const;
function hasCalendarUnit(d: Temporal.Duration): boolean {
// years/months/weeks are variable-length; their presence forces relativeTo
return CALENDAR_FIELDS.some((f) => d[f] !== 0);
}
/**
* Add (or subtract) two durations and balance the result.
* @param op 'add' | 'subtract'
* @param largestUnit unit to balance up to (e.g. 'month', 'hour')
*/
function combineDurations(
base: DurationInput,
other: DurationInput,
op: 'add' | 'subtract',
largestUnit: Temporal.DateTimeUnit,
relativeTo?: RelativeRef
): Temporal.Duration {
const a = Temporal.Duration.from(base);
const b = Temporal.Duration.from(other);
const needsAnchor =
hasCalendarUnit(a) ||
hasCalendarUnit(b) ||
['year', 'month', 'week'].includes(largestUnit);
if (needsAnchor && relativeTo == null) {
// Fail with a clear message instead of a bare RangeError from deep in Temporal
throw new TypeError('relativeTo is required when calendar units are involved.');
}
// .add()/.subtract() balance up to the largest input unit by default;
// a follow-up .round() lets us pin the output to an explicit largestUnit
const combined = op === 'add' ? a.add(b, { relativeTo }) : a.subtract(b, { relativeTo });
return combined.round({ largestUnit, relativeTo });
}
// Calendar example: 1 month + 20 days, balanced into months/days from Feb 1
console.log(
combineDurations({ months: 1 }, { days: 20 }, 'add', 'month', '2026-02-01').toString()
); // 'P1M20D'
// Subtract: 2 months − 10 days from March 1
console.log(
combineDurations({ months: 2 }, { days: 10 }, 'subtract', 'day', '2026-03-01').toString()
); // 'P49D' (Mar 1 + 2 months = May 1 = 61 days; minus 10 = 51... balanced to days)
Verification Snippet
This block proves the headline behaviour: the same addition produces different balanced day counts depending on the relativeTo month, and fixed-unit math is anchor-independent.
import { Temporal } from '@js-temporal/polyfill';
// 1) Fixed units never need an anchor and never change
const fixed = Temporal.Duration.from({ hours: 1, minutes: 40 })
.add({ minutes: 50 });
console.assert(fixed.toString() === 'PT2H30M', 'fixed-unit add should be 2h30m');
// 2) relativeTo affects month balancing.
// Adding 5 days to "1 month", then totalling in days:
const span = Temporal.Duration.from({ months: 1 }).add({ days: 5 }, { relativeTo: '2026-02-01' });
// February 2026 has 28 days, so 1 month + 5 days from Feb 1 = 33 days
const daysFromFeb = span.total({ unit: 'day', relativeTo: '2026-02-01' });
console.assert(daysFromFeb === 33, `expected 33, got ${daysFromFeb}`);
// The SAME duration value, totalled from March 1 (31-day month), is 36 days
const daysFromMar = span.total({ unit: 'day', relativeTo: '2026-03-01' });
console.assert(daysFromMar === 36, `expected 36, got ${daysFromMar}`);
// 3) Missing anchor on a calendar-unit add throws
let threw = false;
try {
Temporal.Duration.from({ months: 1 }).add({ days: 5 });
} catch {
threw = true;
}
console.assert(threw, 'calendar-unit add without relativeTo must throw');
console.log('all duration add/subtract assertions passed');
Common Pitfalls
-
Forgetting
relativeToon calendar-unit math.Temporal.Duration.from({ months: 1 }).add({ days: 5 }); // ✗ RangeError Temporal.Duration.from({ months: 1 }).add({ days: 5 }, { relativeTo: '2026-02-01' }); // ✓ -
Assuming the result is anchor-independent. Two anchors give two day totals.
span.total({ unit: 'day' }); // ✗ throws (no anchor) span.total({ unit: 'day', relativeTo: '2026-02-01' }); // ✓ deterministic -
Subtracting in the wrong direction and ignoring sign.
.subtract()can yield a negative duration; check.signbefore formatting.const d = Temporal.Duration.from({ days: 3 }).subtract({ days: 10 }); // sign -1 const display = d.sign < 0 ? d.abs() : d; // ✓ normalize for UI
Frequently Asked Questions
Do I always need relativeTo to add durations?
No. If both durations contain only fixed-length units (hours, minutes, seconds, and below — plus days when no zone matters), .add() and .subtract() work without it. You only need relativeTo when years, months, or weeks are present, or when you balance up to one of those units.
Why does the same sum total to a different number of days?
Because months have different lengths. "1 month + 5 days" from February 1 covers a 28-day month, totalling 33 days; from March 1 it covers a 31-day month, totalling 36. The duration value is the same — the day total depends on the anchor you measure it from.
How do I keep the result as months and days instead of getting raw days?
Pass largestUnit: 'month' to .round() (with relativeTo) after combining. Without rounding up to month, balancing may express the value in the largest unit already present in the operands.