new Date() and Temporal Both Go Quiet on a Bad Month. You Pick How It Fails.

The month off-by-one is the famous bug. The silent rollover and clamp is the one that costs you.

6 min read

Argued into existence in the Writing Room7 messages · 1 mind changednew Date() and Temporal Both Go Quiet on a Bad Month. You Pick How It Fails.

You asked an AI for a booking app, or a deadline countdown, and it handed you this:

const due = new Date(2026, 12, 1);
console.log(due.toDateString());

You wanted December 1. You got this:

Fri Jan 01 2027

Or maybe you fed it a 1-to-12 value from a form and every month came out one too late. Either way, you're staring at the output wondering whether it's your bug or the AI's. Honest answer: a bit of both, and neither of you is stupid. The search box is full of people asking "new Date(2024, 12, 1) gives January next year" and "JavaScript date month off by one", and the fix takes about a minute.

Then there's a second half most of those answers skip, and it's the part that will actually save your app.

First, two words I'm going to use

An argument is a value you hand to a function inside the brackets. In 'new Date(2026, 12, 1)', the arguments are 2026, 12 and 1: year, month, day.

A month index is a month's position in a list that starts counting at zero. January is index 0, February is 1, and December is 11. JavaScript's Date wants the index, not the number on a calendar.

So the 12 in the broken line above isn't "December". It's the thirteenth slot in a list that only has twelve, and Date doesn't complain. It just keeps counting into next year.

Okay so, the Date fix

Run this. It's all real output from Node 22.22.0 on my box.

console.log(new Date(2026, 11, 9).toDateString()); // Wed Dec 09 2026
console.log(new Date(2026, 10, 9).toDateString()); // Mon Nov 09 2026

Pass 11 and you land on December. Pass 10 and you land on November. If the AI wrote 11 thinking "November", the AI was counting like a human, and Date counts from zero. That's the whole bug.

If the month comes from a form, where people pick 1 to 12, subtract one before it reaches Date:

const formMonth = '11';                         // form values arrive as text
const d = new Date(2026, Number(formMonth) - 1, 9);
console.log(d.toDateString());                  // Mon Nov 09 2026

And when you print a month back out, do the reverse: 'd.getMonth()' gives you 10 for November, so add 1 for display.

That's the fix everyone else tells you, and it works. You can stop here and ship ugly. I mean that.

Why this matters: the number was only half the problem

Here's the part I almost missed. Look at what Date did with 12. It didn't throw an error. It quietly rolled you into January 2027. A wrong-looking date came out and nothing told you the input was bad.

Let's push it. Same three bad-ish inputs, run on Date:

console.log(new Date(2026, 12, 1).toDateString());  // month 12
console.log(new Date(2026, 13, 1).toDateString());  // month 13
console.log(new Date(2026, 1, 31).toDateString());  // February 31st
Fri Jan 01 2027
Mon Feb 01 2027
Tue Mar 03 2026

Month 12 becomes January next year. Month 13 becomes February next year. And February 31st, which doesn't exist, becomes 3 March. Date never says a word. If a typo in your form sends 13, your customer gets a booking in February and you get a support email.

Temporal counts the way you do

Temporal is JavaScript's newer date and time toolkit, built to replace Date. The relevant change for you: its months run 1 to 12. November is 11. Nothing to subtract.

One catch, stated exactly as I ran it. On Node 22.22.0, Temporal is experimental and you have to switch it on with a flag — a flag is an extra word you add after node to turn something on:

node --harmony-temporal month-check.js

Without the flag, 'typeof Temporal' prints undefined on my Node 22, and actually calling it — 'Temporal.PlainDate.from("2026-11-09")' — throws 'ReferenceError: Temporal is not defined', which is the error you'd paste into a search box. With the flag on, you get this:

const nov = Temporal.PlainDate.from('2026-11-09');
console.log(nov.month);   // 11
console.log(nov.toString());   // 2026-11-09

The month you typed is the month you get. The off-by-one is gone, and the first time I saw it I grinned at my terminal.

About newer Node: I haven't run Node 26, so I won't quote its release notes at you. The honest test is the code itself — try it without the flag, and if 'typeof Temporal' still prints undefined, your Node isn't new enough to have it on by default yet. Run 'node -v' to see where you are, use the flag in the meantime, and drop it once 'typeof Temporal' prints 'object'.

Now the same three inputs on Temporal

This is the bit I wanted to show you, because "newer API" is not the same as "safer API." Here's the same trio, using 'PlainDate.from' with an object (the '{ year, month, day }' shape in the curly braces), which is how you'd build a date from form fields:

const tryIt = (label, fn) => {
  try { console.log(label, '->', String(fn())); }
  catch (e) { console.log(label, '-> ' + e.name); }
};

const from = (month, day, opts) =>
  Temporal.PlainDate.from({ year: 2026, month, day }, opts);

tryIt('month 12',            () => from(12, 1));
tryIt('month 13',            () => from(13, 1));
tryIt('month 13, reject',    () => from(13, 1, { overflow: 'reject' }));
tryIt('Feb 31',              () => from(2, 31));
tryIt('Feb 31, reject',      () => from(2, 31, { overflow: 'reject' }));

Here's what Node 22.22.0 printed with the flag on:

month 12 -> 2026-12-01
month 13 -> 2026-12-01
month 13, reject -> RangeError
Feb 31 -> 2026-02-28
Feb 31, reject -> RangeError

Read that slowly.

Month 12 is December, which is correct and which is the whole point. But month 13 did not throw. It quietly became December. February 31st quietly became 28 February. Temporal's default is to clamp: squash a too-big number down to the biggest one that exists.

That's gentler than Date's rolling into next year, but it's still silent. Someone mistyping 13 gets December and never finds out.

Choose loud or quiet, on purpose

The fix for that is the option in the last argument: overflow set to 'reject'. Add it and a bad month or a bad day throws a RangeError, an error that means "this number is outside the allowed range". It stops your program at the exact line with the bad input, which is where you want to find out.

So, here's the whole lesson in one sentence. Temporal fixes the month number. It does not fix bad input. You do that by choosing how it fails.

Neither choice is wrong. For a countdown that displays a date, clamping might be fine. For a booking that takes someone's money, I'd want loud every time. The mistake is not choosing, and letting a default choose for you.

One more thing I tried, because you'll hit it. Giving Temporal a date as a plain string behaves differently:

Temporal.PlainDate.from('2026-02-31');   // RangeError

On my run, the string form of February 31st threw straight away, no option needed. The object form clamped, the string form rejected. Same bad date, two behaviours. Which is exactly why I'd put 'reject' on any date built from user input and stop trusting my memory of what the default does.

Your ten-minute build

Open a file called month-check.js and make it print all five rows from the Temporal section above, plus the three Date lines. Run it twice: once with 'node --harmony-temporal month-check.js' on Node 22, and once plain if you're on Node 26.

Then do the useful bit. Take whichever function your AI wrote that builds a date from form fields and switch it to Temporal with overflow set to 'reject', then feed it 13: if it shouts a RangeError, you've shipped something better than it was this morning. (If you stay on Date and just subtract 1, know that feeding it 13 won't shout — it rolls silently to January, which is the exact bug you came here to stop. Date can't be made loud; that's the reason to move.)

Final thought

The month bug is the famous part. The silent part is the dangerous one.

Next time an AI hands you a date line, do this before you trust it: type the worst number you can think of into it and watch what happens. Whatever it does, you want to have chosen it. Don't let it happen to you.

new Date() and Temporal Both Go Quiet on a Bad Month. You Pick How It Fails. | Vibecodes