Skip to content

How to resolve JavaScript setMonth() giving wrong results when adding months

The Problem

I tried to add one month to a billing date, and I got the wrong result:

problem.js
const billingDate = new Date("2026-01-31");
billingDate.setMonth(billingDate.getMonth() + 1);
console.log(billingDate.toDateString());
// Output: "Mon Mar 02 2026"
// Expected: "Sat Feb 28 2026"

Wait, I added one month to January 31, and I got March 2? That’s not right. I expected February 28.

Let me try a few more examples to understand what’s happening:

testing.js
// Test different month-end scenarios
const tests = [
new Date("2026-01-31"), // Jan 31 + 1 month
new Date("2026-03-31"), // Mar 31 + 1 month
new Date("2026-05-31"), // May 31 + 1 month
new Date("2026-08-31"), // Aug 31 + 1 month
];
tests.forEach((date) => {
const original = date.toDateString();
date.setMonth(date.getMonth() + 1);
console.log(`${original} + 1 month = ${date.toDateString()}`);
});
// Output:
// "Sat Jan 31 2026" + 1 month = "Mon Mar 02 2026" (Feb has 28 days)
// "Tue Mar 31 2026" + 1 month = "Mon May 31 2026" (Apr has 30 days)
// "Sun May 31 2026" + 1 month = "Wed Jul 01 2026" (Jun has 30 days)
// "Mon Aug 31 2026" + 1 month = "Thu Oct 01 2026" (Sep has 30 days)

I see the pattern now: whenever the day of the month exceeds the maximum days in the target month, JavaScript overflows into the next month.

Why This Happens

I realized I misunderstood what setMonth() actually does. Let me dig deeper.

JavaScript’s Date object stores dates as milliseconds since epoch (January 1, 1970). When I call setMonth(), it doesn’t “add a month” - it sets the month property while preserving the day-of-month value.

internals.js
// What I thought setMonth() did:
// "Add 1 month to the current date"
// Result: January 31 -> February 28
// What setMonth() actually does:
// 1. Get current day: 31
// 2. Set month to February (month index 1)
// 3. Keep day as 31
// 4. February 31 doesn't exist
// 5. JavaScript overflows: Feb 31 = Mar 3 (or Mar 2 in 2026)

The root cause: setMonth() sets properties independently. It doesn’t perform date arithmetic. The day value (31) exceeds February’s maximum (28/29), so JavaScript silently overflows into March.

This is documented behavior, but it’s unexpected for developers who assume “adding a month” means “same date next month, or closest valid date.”

Real-World Impact

This isn’t just a theoretical problem. I found a Reddit discussion where developers were confused about expected behavior:

  1. Billing systems: A billing date example like mine produced March 2 when users expected February 28
  2. Two opposing views:
    • “You’re not adding a month, you’re creating a date with a day value higher than exists in that month” - overflow is “correct”
    • “Adding a month usually means same date next month, or closest” - expecting constraining behavior
  3. Financial systems apparently use the February 28 behavior (constrain, not overflow)
  4. Even system tools disagree: GNU date’s date -d "2026-01-31 + 1 month" returns March 3

The Temporal proposal has been in the works for 9 years to fix JavaScript dates. This shows how fundamental this problem is.

How to Fix It

I need three approaches depending on the use case.

Option 1: Clamp to Last Day of Month

If I want the date to stay within the target month, I need to detect overflow and clamp:

add-months-clamp.js
function addMonthsClamp(date, months) {
const result = new Date(date);
const expectedMonth = ((date.getMonth() + months) % 12 + 12) % 12;
result.setMonth(date.getMonth() + months);
// Check if month overflowed
if (result.getMonth() !== expectedMonth) {
result.setDate(0); // Sets to last day of previous month
}
return result;
}
// Test it
const billingDate = new Date("2026-01-31");
const nextMonth = addMonthsClamp(billingDate, 1);
console.log(nextMonth.toDateString()); // "Sat Feb 28 2026"
// More tests
console.log(addMonthsClamp(new Date("2026-03-31"), 1).toDateString()); // "Thu Apr 30 2026"
console.log(addMonthsClamp(new Date("2026-05-31"), 1).toDateString()); // "Tue Jun 30 2026"

The trick: setDate(0) sets the day to the last day of the previous month. So if I’m at March 1 after overflow, calling setDate(0) gives me February 28.

Option 2: Use Day 1 Always (Safest for Billing)

For billing systems, the safest approach is to always work with the first of the month:

add-months-safe.js
function addMonthsSafe(date, months) {
const result = new Date(date);
result.setDate(1); // Reset to first of month first
result.setMonth(result.getMonth() + months);
return result;
}
// Test it
const billingDate = new Date("2026-01-31");
const nextMonth = addMonthsSafe(billingDate, 1);
console.log(nextMonth.toDateString()); // "Sun Feb 01 2026"

This eliminates the overflow problem entirely because day 1 exists in every month.

Option 3: Use a Library

I can use date libraries that handle this correctly:

library-approach.js
// Using date-fns
import { addMonths } from 'date-fns';
const result = addMonths(new Date("2026-01-31"), 1);
console.log(result.toDateString()); // "Sat Feb 28 2026"
// Using Temporal (future standard - still in proposal stage)
const date = Temporal.PlainDate.from('2026-01-31');
const next = date.add({ months: 1 });
console.log(next.toString()); // "2026-02-28"

The Temporal API is the future standard for JavaScript dates. It provides a PlainDate object that separates date values from time zones and handles month arithmetic correctly.

Common Mistakes to Avoid

Looking back at my original code, I made several mistakes:

Mistake 1: Assuming setMonth() adds months

mistake-1.js
// WRONG: setMonth() sets the month property, it doesn't add months
date.setMonth(date.getMonth() + 1);
// RIGHT: Use explicit date arithmetic
function addMonths(date, months) { /* ... */ }

Mistake 2: Not testing with month-end dates

I should always test date logic with edge cases:

test-edge-cases.js
const edgeCases = [
"2026-01-31", // Month with 31 days -> Feb (28 days)
"2026-02-28", // Last day of Feb -> Mar (31 days)
"2024-02-29", // Leap year Feb 29
"2026-03-31", // 31 days -> Apr (30 days)
"2026-10-31", // 31 days -> Nov (30 days)
];

Mistake 3: Ignoring leap years

leap-year.js
// 2024 is a leap year
const leapYear = new Date("2024-01-31");
leapYear.setMonth(leapYear.getMonth() + 1);
console.log(leapYear.toDateString()); // "Fri Mar 02 2024" (Feb has 29 days)
// 2026 is not a leap year
const normalYear = new Date("2026-01-31");
normalYear.setMonth(normalYear.getMonth() + 1);
console.log(normalYear.toDateString()); // "Mon Mar 02 2026" (Feb has 28 days)

Mistake 4: Using new Date(year, month, day) without validation

mistake-4.js
// This also silently overflows
const date = new Date(2026, 1, 31); // February 31
console.log(date.toDateString()); // "Mon Mar 02 2026"
// Always validate or use ISO strings
const validDate = new Date("2026-02-28");

When This Matters

This issue affects real systems:

  • Billing systems: Recurring charges need predictable behavior
  • Legal compliance: Contract end dates must be unambiguous
  • User trust: Silent date changes erode confidence
  • International systems: Different locales have different expectations

The Reddit comment about financial systems using February 28 behavior proves this isn’t theoretical - real money depends on predictable date arithmetic.

Summary

JavaScript’s setMonth() sets the month property without constraining invalid dates, causing overflow when the day exceeds the new month’s maximum. The fix depends on the use case:

  1. Clamp to last day for “closest date” semantics
  2. Use day 1 for billing safety
  3. Use a library (date-fns, Luxon, or Temporal) for robust date arithmetic

The key insight: setMonth() doesn’t add months - it sets the month property. Understanding this distinction prevents subtle bugs in production code.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments