Dates in JavaScript Without Losing a Day
Why a Date is a moment and not a day, how the off-by-one bug happens in both directions, and how to store, build and show dates safely. After this, a due date stays on the day the customer picked.
Course content
Sign in to save your progress.
-
Set Up Projects That Don't Break Each Other
-
Node.js and npm: Running JavaScript Outside the Browser
-
package.json and Lock Files: Same Install Every Time
-
-
-
Organize Code Other People Can Read
-
JavaScript Modules: import, export and Splitting One Big File
-
JavaScript Classes: When an Object Literal Is No Longer Enough
-
Destructuring and Spread Without the Confusion
-
-
-
Work With Real Data
-
Array Methods: map, filter, reduce and When a Loop Is Clearer
-
Working With JSON: parse, stringify and What Breaks
-
Dates in JavaScript Without Losing a Day
-
-
-
Waiting for Things
-
Callbacks to Promises: Why the Code Runs Out of Order
-
async/await and fetch: Calling a Real API
-
-
-
Prove It Works Before Someone Else Finds Out
-
try/catch and Custom Errors
-
Your First Tests With Vitest
-
-
- Final Check: JavaScript Intermediate
Skip ahead: If you already know why new Date("2026-10-15") can show the 14th, skip to "Showing dates to people".
A customer in New York picks October 15 as a delivery date. Your page shows October 14. Nobody changed anything. The code is "correct". This is the most common date bug in JavaScript, and it comes from one fact: a Date isn't a calendar day. It's a single moment in time, stored as milliseconds since 1 January 1970 UTC. Every time you show it, JavaScript converts that moment to some time zone. Pick the wrong one and the day moves.
The bug, reproduced
const delivery = new Date("2026-10-15");
console.log(delivery.toString());
console.log(delivery.getDate());
Wed Oct 14 2026 20:00:00 GMT-0400 (Eastern Daylight Time)
14
A date-only string like "2026-10-15" is read as midnight UTC. In New York that moment is 8 PM the day before. getDate() uses the computer's local time zone, so it says 14.
The Philippines is ahead of UTC, so here the same line shows the right day. Don't relax. The bug runs the other way:
const due = new Date(2026, 9, 15);
console.log(due.toString());
console.log(due.toISOString().slice(0, 10));
Thu Oct 15 2026 00:00:00 GMT+0800 (Philippine Standard Time)
2026-10-14
new Date(2026, 9, 15) is midnight local time (and yes, month 9 is October, months start at 0). toISOString() converts to UTC, which is 4 PM on the 14th. Slice off the date part and you've sent the 14th to your server.
So the problem isn't one function. It's mixing local time and UTC without noticing. The tutorial Why Your Date Is Off by One Day has a checklist for finding which half of your code is doing it.
Rule 1: decide if it's a day or a moment
- A calendar day (birthday, due date, delivery date, holiday): there's no time and no zone. "October 15" is October 15 for everyone. Keep it as the string
"2026-10-15"as long as you can. Don't turn it into aDatejust to store or send it. - A moment (order placed, payment received, login): something happened at one instant. Store it in UTC, as an ISO string like
"2026-10-15T02:30:00.000Z", and convert to local time only when you show it.
Most date bugs are a calendar day being treated as a moment.
Rule 2: if you must build a day, build it in one zone
To turn a local Date back into YYYY-MM-DD, don't use toISOString. Use the local parts:
const due = new Date(2026, 9, 15);
const pad = n => String(n).padStart(2, "0");
console.log(`${due.getFullYear()}-${pad(due.getMonth() + 1)}-${pad(due.getDate())}`);
console.log(due.toLocaleDateString("en-CA"));
2026-10-15
2026-10-15
The en-CA locale happens to format as YYYY-MM-DD. It's a known shortcut. The manual version is clearer about what it does.
⚠ Warning: Never parse dates like "15/10/2026" or "10/15/2026" with new Date(). The first gives Invalid Date in Node and Chrome. The second works only because it's read as US month/day, which is wrong for most of the world, including the Philippines. If you get dates in that format, split them yourself and build the date with numbers.
Showing dates to people
Use Intl.DateTimeFormat (or toLocaleString) and say which time zone you mean:
const placed = new Date("2026-10-15T02:30:00Z");
const manila = new Intl.DateTimeFormat("en-PH", {
dateStyle: "medium",
timeStyle: "short",
timeZone: "Asia/Manila",
});
console.log(manila.format(placed));
console.log(placed.toLocaleString("en-US", { timeZone: "America/New_York" }));
Oct 15, 2026, 10:30 AM
10/14/2026, 10:30:00 PM
Same moment, two correct answers. Leave out timeZone and you get the zone of whatever computer runs the code: the visitor's browser, or a server that's often set to UTC. That's why a report can be right on your laptop and wrong on the server.
Date math that surprises people
const d = new Date(2026, 0, 31);
d.setMonth(d.getMonth() + 1);
console.log(d.toDateString());
Tue Mar 03 2026
January 31 plus one month is "February 31", which JavaScript rolls over to March 3. If you're adding months for billing or subscriptions, decide what should happen at the end of the month and write that rule yourself, or use a date library.
💡 Tip: For anything beyond showing and comparing dates (time zone conversions, adding business days, recurring schedules), use a library like date-fns or dayjs. Temporal, the built-in replacement for Date, fixes most of this, but check that it's available in the browsers and Node version you support before you rely on it.
Common mistakes
- Treating a due date as a moment and storing it with a time.
- Using
toISOString().slice(0, 10)to get "today". - Forgetting months start at 0.
- Formatting without
timeZoneon a server. - Comparing dates with
===(that compares objects). Comparea.getTime() === b.getTime().
What happens if you skip this
It works in testing, because you test in one time zone. Then a customer abroad, or your own server running in UTC, gets the day before. Deliveries go out a day early, invoices fall into the wrong month, and the bug only appears for some users at some hours.
Your server runs in UTC and your users are in Manila. An order is placed at 7:30 AM Manila time on the 1st. Which day does a report that uses getDate() on the server put it on?
About
Why a Date is a moment and not a day, how the off-by-one bug happens in both directions, and how to store, build and show dates safely. After this, a due date stays on the day the customer picked.
- Duration
- 5 min
- Last update
- 08/25/2026
Comments (0)
Sign in to commentNo comments yet.