How wrong can a JavaScript Date calculation go?

Published: (January 14, 2026 at 05:46 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

The issue

In January 2025 I was in Santa Clara, California writing some JavaScript to perform reporting. I wanted to get a number of events that happened within a month, so I created a date object for the first day of the month, added one month to it, and then subtracted a day to get the last day. Seems straightforward, right?

I got a really weird result though. I reduced the issue to the following code.

const date = new Date("2024-01-01T00:00:00.000Z");
date.toISOString(); // => "2024-01-01T00:00:00.000Z" as expected
date.setMonth(1);
date.toISOString(); // => "2023-03-04-T00:00:00.000Z" WTF?

I added a month to the 1 January 2024 and landed on the 4th March 2023. What happened?

Times and zones

You might think it odd that I set the scene on the West Coast of the US, but it mattered. This code would have run fine in UTC and everywhere east of it.

JavaScript dates are more than just dates; they also contain a time. Even though I only wanted to deal with days and months, the time still mattered.

Midnight on 1 January 2024 in UTC is still 4 pm on 31 December 2023 in Pacific Time (UTC‑8). date.setMonth(1) sets the month to February (months are 0‑indexed). But we started on 31 December 2023, so JavaScript has to handle the non‑existent date of 31 February 2023. It overflows to the next month, giving 3 March. Finally, when printed, the date is translated back into UTC, resulting in midnight on 4 March 2023.

All of these steps are reasonable when broken down; the confusion stems from the unexpected result.

Always use UTC

Since I didn’t care about the time and wanted to work in UTC, I fixed the code using the Date object’s setUTCMonth method. My original code subtracted a day to get the last day of a month, so I also used setUTCDate. All set${timePeriod} methods have a setUTC${timePeriod} counterpart.

const date = new Date("2024-01-01T00:00:00.000Z");
date.toISOString(); // => "2024-01-01T00:00:00.000Z"
date.setUTCMonth(1);
date.toISOString(); // => "2024-02-01-T00:00:00.000Z"

This fixed the issue.

Bring on Temporal

One reason the original code went wrong was that I was manipulating dates and times without thinking about it. Temporal provides objects specifically for calendar dates without a time zone.

Using Temporal.PlainDate to represent a calendar date simplifies things. Rather than setting months and dates or adding milliseconds, you add a duration. Temporal objects are immutable, so each change returns a new object.

const startDate = Temporal.PlainDate.from("2024-01-01"); // => Temporal.PlainDate 2024-01-01
const nextMonth = startDate.add({ months: 1 });        // => Temporal.PlainDate 2024-02-01
const endDate   = nextMonth.subtract({ days: 1 });   // => Temporal.PlainDate 2024-01-31

Date manipulation without worrying about times—wonderful!

Mind the time zone

Temporal is rolling out to JavaScript engines. At the time of writing it is available in Firefox and just landed in Chrome 144. It is also listed as behind a flag in Safari Technical Preview. To test it locally, open Firefox or Chrome, or use one of the polyfills:

If you still have to use Date, keep your time zone in mind. Consider moving to, or at least learning how to use, Temporal now. Even when you try to avoid them, time zones can give you a headache.

Back to Blog

Related posts

Read more »