Your JS Date Is Lying to You
Your JS Date Is Lying to You
Tue Jul 21 2026
tutorials
javascript
dates
temporal
... is lying to you
Run this in your browser console:
const d1 = new Date('2026-07-21')<br>console.log(d1.getDate())
If you are in a timezone west of UTC, you will see 20, not 21. The string looks unambiguous, but Date parsed it as UTC midnight, then getDate() converted to your local time and moved the clock back. You asked for July 21st, but got July 20th.
Or try the constructor directly:
const d2 = new Date(2026, 7, 21)<br>console.log(d2.toDateString()) // "Fri Aug 21 2026"
Months are 0-indexed, so 7 is August, not July. You probably asked for July 21st and got August 21st.
And if you needed a reminder that Date objects are mutable:
const deadline = new Date('2026-07-21')<br>deadline.setDate(32)<br>console.log(deadline) // 2026-08-01
No error, just silent rollover.
These are default behavior of the most-used date API in the world, quietly corrupting date logic in production applications for thirty years. Date was written in ten days in 1995, modeled closely on the Java java.util.Date class (which Java later deprecated). The web grew around it, frameworks worked around it, and the traps became invisible because everyone assumed the API made sense.
This post works through the main ways Date misleads you - parsing, mutation, timezones, arithmetic, and serialization - and shows how the Temporal API, now available in modern runtimes, fixes each one. Along the way, it also covers safe patterns for code that is still stuck on Date and cannot migrate yet.
Parsing Is Unreliable
The ECMAScript spec says new Date(string) must support one format: ISO 8601 date-time strings like "2026-07-21T12:00:00Z", everything else is implementation-defined. In practice every engine accepts a wide range of informal strings — "July 21, 2026", "21/07/2026", "07-21-2026", but they don't agree on all of them, and the spec does not require them to.
That sounds like an edge case until you hit the most common one: a plain date string.
new Date('2026-07-21') // parsed as UTC midnight<br>new Date('2026/07/21') // parsed as local midnight (or Invalid Date in some engines)<br>new Date('July 21 2026') // parsed as local midnight
The ISO date-only format YYYY-MM-DD is treated as UTC by the spec. All other date strings are treated as local time, if they parse at all. So two strings that look functionally identical to a human can produce timestamps twelve hours apart depending on which separator you happened to use. The first one will shift dates when you call getDate(), getMonth(), or any other local-time method while the user is outside UTC. The second may throw Invalid Date in one runtime and succeed in another.
Invalid Date is its own problem. Date does not throw on a bad string:
const d = new Date('not a date')<br>console.log(d) // Invalid Date<br>console.log(isNaN(d)) // true
The object is constructed. It breaks later, silently, when you try to use the value - typically as a NaN that propagates through arithmetic, or as the string "Invalid Date" that ends up stored in a database or sent to an API.
This is why nearly every production codebase that deals seriously with dates ends up reaching for a library. date-fns, dayjs, and luxon all provide strict parsing functions that throw explicitly when a string does not match the expected format. That behavior is what Date should have had from the start.
Temporal is JavaScript's next-generation built-in date and time API (Temporal.*), designed to replace the sharp edges of Date. At the time of writing, its normative text is still maintained in the proposal repository while final ECMA-262 publication catches up, but engines and polyfills can still implement it because the Stage 4 semantics are already fixed. Temporal.PlainDate.from('2026-07-21') parses one format and always means a calendar date with no timezone involved. Temporal.Instant.from('2026-07-21T00:00:00Z') requires an explicit UTC offset. Ambiguous input becomes a parse error instead of a silent timezone shift.
That closes the parsing story. For the next sections, we stay on Date because most existing codebases still run on it, and these bugs are where production incidents usually happen. Temporal appears as a reference model for safer semantics, not as an assumption that every project can migrate immediately.
0-Based Months and Mutation Break Assumptions
Even when you construct a Date correctly, month indexing and mutation rules still create production bugs.
As shown in the intro example (new Date(2026, 7, 21)), constructor month values are 0-based, so 7 means August, creating round-trip errors:
function toApiDateParts(d) {<br>return {<br>year: d.getFullYear(),<br>month: d.getMonth(), // returns 0..11<br>day: d.getDate()
console.log(toApiDateParts(new Date('2026-12-15T00:00:00Z')))<br>// { year: 2026, month: 11, day: 15 }
If another service expects month: 12, your December data is now November data.
Mutation makes this worse...