"What date is 1 month from today?"
For March 24, the intuitive answer is April 24. Simple. But what about January 31? Is one month later February 28? February 31? March 3?
Date arithmetic involving months is genuinely ambiguous, and the way JavaScript handles it will surprise you.
What JavaScript Actually Does
const d = new Date('2026-01-31');
d.setMonth(d.getMonth() + 1); // "Add 1 month"
console.log(d.toISOString()); // "2026-03-03T00:00:00.000Z"
You asked for February, you got March 3. What happened?
JavaScript's setMonth works by setting the month field and letting overflow cascade. February has 28 days in 2026, so "February 31" overflows to March 3. This behavior is defined in the spec — it's not a bug.
But it's almost certainly not what your users expect.
Three Different Conventions
There's no universally correct answer to "Jan 31 + 1 month." Different systems use different conventions:
| Convention | Result | Used by |
|---|---|---|
| Overflow (JS default) | March 3 | Raw JS Date math |
| Clamp to last day | February 28 | Most calendar apps, banking |
| Snap to last day of target | February 28 | PostgreSQL interval, many ORMs |
For human-facing applications, clamping to the last valid day is almost always what you want.
The Correct Implementation
function addMonths(date, months) {
const result = new Date(date);
const targetMonth = result.getMonth() + months;
result.setMonth(targetMonth);
// If the month overflowed, we went too far — clamp to last day
// of the intended month
if (result.getMonth() !== ((targetMonth % 12) + 12) % 12) {
result.setDate(0); // setDate(0) = last day of previous month
}
return result;
}
addMonths(new Date('2026-01-31'), 1);
// → February 28, 2026 ✓
addMonths(new Date('2026-01-31'), 2);
// → March 31, 2026 ✓
addMonths(new Date('2026-03-31'), -1);
// → February 28, 2026 ✓
Edge Cases That Will Find You
Leap years:
addMonths(new Date('2024-01-31'), 1);
// → February 29, 2024 ✓ (2024 is a leap year)
addMonths(new Date('2024-02-29'), 12);
// → February 28, 2025 ✓ (2025 is not a leap year)
Adding months then subtracting doesn't always round-trip:
const start = new Date('2026-01-31');
const plusOne = addMonths(start, 1); // Feb 28
const backOne = addMonths(plusOne, -1); // Jan 28 — NOT Jan 31!
This is mathematically unavoidable. If you need round-trip consistency, you have to store the "intended" day separately.
Subscription billing example:
// Customer subscribes Jan 31
// Monthly billing dates:
// Jan 31 → Feb 28 → Mar 28 → Apr 28 ...
// Instead of: Jan 31 → Feb 28 → Mar 31 → Apr 30
For billing, many systems store the "anchor day" (31) and compute each billing date as: min(anchor_day, days_in_target_month). That way March and April get their correct end-of-month dates.
function nextBillingDate(anchorDay, fromDate) {
const next = new Date(fromDate);
next.setMonth(next.getMonth() + 1, 1); // move to 1st of next month
const daysInMonth = new Date(next.getFullYear(), next.getMonth() + 1, 0).getDate();
next.setDate(Math.min(anchorDay, daysInMonth));
return next;
}
nextBillingDate(31, new Date('2026-01-31')); // Feb 28
nextBillingDate(31, new Date('2026-02-28')); // Mar 31
nextBillingDate(31, new Date('2026-03-31')); // Apr 30
Adding Years Has the Same Problem
const d = new Date('2024-02-29'); // leap day
d.setFullYear(d.getFullYear() + 1);
console.log(d); // March 1, 2025 — overflowed
Same fix: check for overflow after setting, clamp if needed.
When You Just Need the Answer
For quick calculations — "what's the date 3 months from now?", "when does my annual subscription renew?" — datetimecalculator.app handles the clamping correctly. Useful for sanity-checking your implementation against a known-good result.
The Takeaway
-
setMonth()overflows silently — never use it directly for user-facing date math - Define your overflow convention explicitly, document it, and test the edge cases
- End-of-month dates + month arithmetic = you need a clamping strategy
- Round-trip consistency (add then subtract) is impossible when months overflow; design around this
What date arithmetic gotcha has caused you the most grief? Month-end billing edge cases seem to be a rite of passage.
Top comments (0)