Checked against official sources on October 30, 2026
A user picks November 2. Your app shows November 1. Nobody touched the date, and yet it moved.
This one has been around for years. A Stack Overflow question titled "Is the Javascript date object always one day off?" was asked in September 2011. When we opened it on October 30, 2026, it had over 380,000 views and a score above 470.
We wrote a tiny repro and ran it with Node v24.22.1 under four time zones: Los Angeles, New York, UTC and Kolkata. All output below is copied from that run.
What is going on, in plain words
A Date in JavaScript is not a calendar day. It is one exact moment in time, stored as milliseconds since 1970 in UTC. UTC is the world's base clock, with no time zone offset.
So when you give it just a day, like "2026-11-02", JavaScript has to pick a moment. It picks midnight UTC.
MDN's Date reference says it directly: when there is no time zone offset, date-only strings are read as UTC, while date-time strings are read as local time. MDN calls this a "historical spec error". In other words, it was a mistake in the rules that can no longer be changed without breaking old sites.
Midnight UTC is still the evening of the day before in the Americas. When you show that moment in local time, you get the earlier date.
The bug, run in four places
const due = "2026-11-02"; const d = new Date(due); console.log(d.toISOString(), d.getDate(), d.toLocaleDateString("en-US")); TZ=America/Los_Angeles toISOString: 2026-11-02T00:00:00.000Z getDate(): 1 shown: 11/1/2026 TZ=America/New_York toISOString: 2026-11-02T00:00:00.000Z getDate(): 1 shown: 11/1/2026 TZ=UTC toISOString: 2026-11-02T00:00:00.000Z getDate(): 2 shown: 11/2/2026 TZ=Asia/Kolkata toISOString: 2026-11-02T00:00:00.000Z getDate(): 2 shown: 11/2/2026 The stored moment is the same everywhere. Only the shown day changes. In the two US zones, it is one day early.
Notice that Kolkata looks fine here. That is why this bug slips past teams: whoever tests east of UTC sees nothing wrong.
Add a time and the rule flips
Now pass the same day with a time and no Z on the end:
const d = new Date("2026-11-02T00:00:00"); TZ=America/Los_Angeles getDate(): 2 toISOString: 2026-11-02T08:00:00.000Z TZ=America/New_York getDate(): 2 toISOString: 2026-11-02T05:00:00.000Z TZ=UTC getDate(): 2 toISOString: 2026-11-02T00:00:00.000Z TZ=Asia/Kolkata getDate(): 2 toISOString: 2026-11-01T18:30:00.000Z This time it is read as local midnight, so the day is right everywhere. Two strings that look almost the same follow two different rules.
The same bug, going the other way
Most people meet the bug when they show a date. Some meet it when they save one. A common habit is to turn a Date into "YYYY-MM-DD" with toISOString().slice(0, 10).
const picked = new Date(2026, 10, 2); // months start at 0, so 10 is November console.log(picked.toISOString().slice(0, 10)); TZ=America/Los_Angeles saved as: 2026-11-02 TZ=America/New_York saved as: 2026-11-02 TZ=UTC saved as: 2026-11-02 TZ=Asia/Kolkata saved as: 2026-11-01 Now the US zones are fine, and India is a day early. Local midnight in Kolkata is 18:30 the evening before in UTC, and toISOString() always speaks UTC.
So each half of the bug hides from a different team. Put both halves in one app and you can lose a day on the way in and on the way out.
The fix we use: treat a day as a day
If the value is a plain calendar day, like a birthday, a due date, or a start date, do not let it become a UTC moment. Split the string and build the date from its parts. The new Date(year, monthIndex, day) form always uses local time.
function parseDay(s) { // "2026-11-02" -> local midnight const [y, m, d] = s.split("-").map(Number); return new Date(y, m - 1, d); } function formatDay(date) { // local Date -> "2026-11-02" const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } TZ=America/Los_Angeles parseDay: 11/2/2026 round trip: 2026-11-02 UTC display: 11/2/2026 TZ=America/New_York parseDay: 11/2/2026 round trip: 2026-11-02 UTC display: 11/2/2026 TZ=UTC parseDay: 11/2/2026 round trip: 2026-11-02 UTC display: 11/2/2026 TZ=Asia/Kolkata parseDay: 11/2/2026 round trip: 2026-11-02 UTC display: 11/2/2026 Same day in all four zones, both ways.
The "UTC display" column is the other common fix. Keep new Date("2026-11-02"), but show it in UTC too, so both sides agree:
new Date("2026-11-02").toLocaleDateString("en-US", { timeZone: "UTC" }); That works, but you have to remember timeZone: "UTC" on every display call, and the getUTCDate() family on every read. Miss one, and the bug is back. We find the parse-into-parts version harder to get wrong.
Bonus trap: "add one day" is not "add 24 hours"
While a date is a moment, adding a day by adding milliseconds is risky. Most US clocks fall back one hour on Sunday, November 1, 2026. Look:
const start = new Date(2026, 10, 1); const plus24h = new Date(start.getTime() + 24 * 60 * 60 * 1000); const nextDay = new Date(start); nextDay.setDate(start.getDate() + 1); TZ=America/Los_Angeles +24h: Sun Nov 01 2026 23:00:00 setDate+1: Mon Nov 02 2026 00:00:00 TZ=America/New_York +24h: Sun Nov 01 2026 23:00:00 setDate+1: Mon Nov 02 2026 00:00:00 TZ=UTC +24h: Mon Nov 02 2026 00:00:00 setDate+1: Mon Nov 02 2026 00:00:00 TZ=Asia/Kolkata +24h: Mon Nov 02 2026 00:00:00 setDate+1: Mon Nov 02 2026 00:00:00 In Los Angeles and New York, that Sunday had 25 hours. Adding 24 hours left us at 11 PM on the same day. setDate(getDate() + 1) moves by one calendar day, so it landed on Monday.
Where this is going: Temporal
MDN lists Temporal as the newer date API. It has a type called PlainDate that is just a day, with no time and no zone, which is exactly what a due date is. In Node v24.22.1 it is still behind a flag, so we ran it with node --harmony-temporal:
const day = Temporal.PlainDate.from("2026-11-02"); console.log(day.toString(), day.day, day.add({ days: 1 }).toString()); TZ=America/Los_Angeles PlainDate: 2026-11-02 day: 2 next: 2026-11-03 TZ=Asia/Kolkata PlainDate: 2026-11-02 day: 2 next: 2026-11-03 Check support in your own runtime and browsers before you ship it.
Quick checks for your codebase
- Search for
new Date("with a date-only string. Each one is a suspect. - Search for
toISOString().slice(0, 10)and.split("T")[0]. Each one is a suspect too. - Run your tests twice, with
TZ=America/Los_AngelesandTZ=Asia/Kolkata. One zone west of UTC and one east of it catches both halves. - Decide, per field, whether it is a calendar day or an exact moment. Store days as
"YYYY-MM-DD"text and moments as full timestamps with an offset.
Docs and threads we opened
- Stack Overflow: Is the Javascript date object always one day off?
- MDN: Date() constructor
- MDN: Date, "Date time string format"
About TalentOp Systems LLC
Our team works out of Omaha, Nebraska, and we have been in business since February 2025. We do IT staffing, software consulting and project development, and we staff software roles for clients in banking, insurance, healthcare and retail, where one wrong day on a due date or a policy start date is a real problem. Roles can be W2 or C2C, and many run 12+ months.
TalentOp Systems is a direct vendor for Kroger, whose offices are in Blue Ash, Ohio.
E-Verify: yes, TalentOp Systems LLC is enrolled and E-Verified.
Need H-1B sponsorship? TalentOp Systems LLC provides it for hires who meet the role's requirements and eligibility.
We also take H-1B transfers, and OPT or STEM OPT candidates are welcome to reach out.
Find us at www.talentopsystems.com or email info@talentopsystems.com.
