DEV Community

HAU
HAU

Posted on

The Right Way to Add Months to a Date in JavaScript (It's Not What You Think)

"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"
Enter fullscreen mode Exit fullscreen mode

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 ✓
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)