JS Date constructor timezone oddity

Photo by Luis Cortes on Unsplash

JS Date constructor timezone oddity

new Date('YYYY-MM-DD') might not do what you think it does

For a long time, I only did Date manipulation in JS via some date library that wrapped over the native Date object. There were always a lot of missing features on the native Date. But for some time now I've been listening to how date libraries are rarely necessary these days.

After my short encounter with it, I wouldn't say so. I wasn't aware of how well-protected we all were by those battle-tested date libraries. It's such an easy way to shoot yourself in the foot using plain Date objects. This example alone is enough to make you rethink some things:

new Date('2024-01-01')

What do you think this does? The date format is OK. The date should be parsed and we should get a Date object with the specified date set internally, right? Well, correct to some extent, but it depends on the time zone...

What if I told you that you could end up with a 2023-12-31 when converting that object to a string? If you don't know about Date quirks it's almost a certainty you will end up with this bug eventually.

What if I told you that you could end up with two different dates for these 2 inputs to the Date constructor:

  • 2024-01-01

  • 2024-01-01T00:00:00

This is the reality in which we as web devs live. It's not commonly known behavior unless you worked with something that required a deeper knowledge of the Date internals. This is something that caused a bug in my code the other day. It was reported by users that are behind the UTC. Now, why would that be the case?

It happens because Date assumes the time zone differently based on having time specified vs. the case with no time specified. And by assuming an input in the wrong time zone it ends up with the wrong date being set internally(shifted by a difference in a client's time zone offset to the UTC).

Look at this example, where I'm running the JS on the client located in the UTC+1 zone:

Do you notice any difference? :)

It's weird, it's unexpected, it's unintuitive. But, does it have some meaningful background, maybe some well-defined standard to back it up? Also no. This is a remnant of the old JS world, an error in specification that does not adhere to the ISO 8601 standard and never will because of backward compatibility issues.

new Date('2024-01-01') should do the same as new Date('2024-01-01T00:00:00') but it doesn't. Without time explicitly being set, it assumes that the input parameter('2024-01-01') is in UTC. Now if that code runs on the client that's behind UTC, for example, a western part of the globe. If the client's time zone lags for e.g. 6 hours, the Date constructor will produce a new Date object with the date/time being set to 2023-12-31T18:00:00. On the other hand, if you pass in the 2024-01-01T00:00:00 string(notice that one also doesn't specify a time zone) it won't assume UTC but rather use your local time zone offset and you will get the date object with the exact date/time for your time zone :).

This is where the bugs come from. Date constructor assumes UTC time zone for one input format while on the other hand, it uses the local time zone for the format that has time specified. Pretty unexpected. If you try to do the same inputs using moment.js you will get the expected and consistent behavior because date libraries handle so many weird cases for us out of the box.

It was a bit hard to dig out the exact reason why it behaves like this, but I eventually found the reason on MDN page:

Also, see the mentioned post on the topic and the proposal for a solution — proposed Temporal API. But at the moment this is the situation with Temporal API:

Leaving us with plenty of reasons to still choose to work with external libraries for dates. Just like the following couple of reasons:

// 1. Months are zero-indexed, while days and years are one-indexed
const date = new Date(2022, 0, 1); // January 1, 2022

// 2. Two-digit years are interpreted as 1900-1999
const y2k = new Date(99, 0, 1); // January 1, 1999

// 3. Negative years are allowed, representing years B.C.
const ancient = new Date(-44, 2, 15); // March 15, 45 B.C.

// 4. Parsing date strings can be inconsistent across browsers and locales
const parsedDate = new Date('03/15/2022'); // May not work consistently

// 5. Daylight Saving Time can cause unexpected behavior
const dst = new Date(2021, 2, 14, 2, 30); // March 14, 2021, 03:30 (skipped hour)

By the way, please consider a bit more modern options for handling dates like Day.js or date-fns. I mentioned moment.js just because it was the easiest to reach and verify the behavior, but other than that its usage is no longer recommended.