Why are 2025/05/28 and 2025-05-28 different days in JavaScript?
While setting up this site itself, I ran into the following oddity:
console.log(new Date('2025/05/28').toDateString()); // Wed May 28 2025
console.log(new Date('2025-05-28').toDateString()); // Tue May 27 2025
// Bonus: (omit leading 0)
console.log(new Date('2025-5-28').toDateString()); // Wed May 28 2025
You may get different results on your machine!
What's going on?
A Date
in JavaScript always represents a point in time (i.e. milliseconds since epoch). This is more apparent when printing out the full date string:
const date = new Date('2025/05/28');
console.log(date); // Wed May 28 2025 00:00:00 GMT-0700 (Pacific Daylight Time)
console.log(date.toDateString()); // Wed May 28 2025
In this case, the passed-in date string is being interpreted as a timestamp in my local time zone. toDateString()
also operates relative to the local time and so we get the same day-of-the-month back out.
The difference with '2025-05-28'
is in parsing behavior; the string is interpreted as UTC and so ends up at a different point in time:
const date = new Date('2025-05-28');
console.log(date); // Tue May 27 2025 17:00:00 GMT-0700 (Pacific Daylight Time)
console.log(date.toDateString()); // Tue May 27 2025
Why the discrepancy?
The misadventures of browser date-parsing
After digging through the code and commit histories of Chrome/Firefox/Safari, I’ve reconstructed a timeline:
- In 2009, these browsers supported parsing a mishmash of date-time formats. When time zone offsets are not explicitly specified in the string, they all fall back to using local time, including for a date string like
'2025/05/28'
. - ES5, to be released at the end of the year, includes a requirement for supporting a new standardized date-time format based heavily off of ISO 8601. This format is broken up into date-only forms like
'2025-05-28'
and date-time forms like'2025-05-27T17:00-07:00'
where the ending UTC offset is optional.- What does the spec say about time zone interpretation for date-only forms (which never have an offset) or date-time forms missing an offset? Only that
The String may be interpreted as a local time, a UTC time, or a time in some other time zone, depending on the contents of the String.
(Gee, thanks…)
- What does the spec say about time zone interpretation for date-only forms (which never have an offset) or date-time forms missing an offset? Only that
- Firefox is the first to implement this requirement. They choose to interpret date-only forms as UTC and date-time forms missing an offset as local time. Not only is there now a discrepancy between
'2025/05/28'
and'2025-05-28'
, but also surprising behavior like:console.log(new Date('2025-05-28')); // Tue May 27 2025 17:00:00 GMT-0700 (Pacific Daylight Time) console.log(new Date('2025-05-28T00:00')); // Wed May 28 2025 00:00:00 GMT-0700 (Pacific Daylight Time)
- Chrome is next, choosing to use local time for both.
- Safari is next, but its parsing logic incorrectly requires that all date, time, and offset fields be present.
- ES5.1 releases in mid-2011 and now additionally mentions that
The value of an absent time zone offset is Z.
- Chrome updates its implementation to use UTC for both cases.
- Safari fixes the earlier bug and uses UTC for both cases.
- A bug is filed against the spec itself, pointing out that ISO 8601 represents date-times without offsets as local time. ES6 in 2015 replaces the ES5.1 addition with
If the time zone offset is absent, the date-time is interpreted as a local time.
- Chrome switches back to using local time for both cases.
- A bug is filed against Chrome for breaking backwards compatibility when parsing date-only forms. They revert the previous change.
- Chrome files an issue against the spec and after discussion, it’s decided to switch date-only forms back to UTC but leave date-time forms without offset as local (i.e. Firefox’s behavior).
- ES7 releases with the updated requirement. Chrome makes the change and then eventually, Safari.
This behavior has been maintained to the present day where every possible string accepted by the Date
constructor falls back to local time except valid ISO date-strings like '2025-05-28'
.
What’s interesting looking at the timeline is that despite being designed as a standardized format, from its release in 2009 up until early 2020, there would never exist a point where the major browsers behaved consistently for missing offsets. Meanwhile, Chrome has flipped hilariously from local → UTC → local → UTC → local when parsing '2025-05-28T00:00'
. And all this just to settle at Firefox’s 2009 behavior which, in my opinion, is the most unintuitive of them all.
What about Temporal?
For the unaware, JavaScript Temporal is coming: a new set of date and time APIs intended to replace the Date
object.
Our whole original date parsing issue stemmed from time zone ambiguity but in many cases, the desire is to treat date-only strings as exactly that — dates only. For example, when I say that Christmas this year is 2025-12-25
, I don’t mean the universal instant in time that is 2025-12-25T00:00:00.000Z
.
While Date
can only ever represent the latter, Temporal offers the option of plain dates (i.e. a date without a time zone). '2025-12-25'
is just 2025-12-25
, side-stepping the parsing ambiguity issue entirely.
But what if one really wants to parse a date-only string into an instant in time? What time zone will Temporal choose when absent in the string itself?
Answer: It’s a hard error; an offset or time zone identifier must be provided. No repeat mistakes here.
Bonus: enter the cursed zone
One thing I never realized until reading browser date-parsing source code is just how lenient it can be.
Here’s a fun example for Chrome/Firefox: can you spot why this (valid!) date string is being parsed as the month of May?
const date = 'it is wednesday, my dudes. 2025, April, maybe...28(?)';
console.log(new Date(date)); // Wed May 28 2025 00:00:00 GMT-0700 (Pacific Daylight Time)