Process

Why Your Date Is Off by One Day: The JavaScript Timezone Bug

A birthday or due date that shows up one day early is almost always a date-only value that got treated as a moment in time. Here is why it happens, where it hides, and how to stop it.

6 min readProcess

A user enters a birthday of 15 March. Your app saves it, loads it back, and shows 14 March. Nobody changed the data. Nobody made a typo. The date just moved.

If you searched for "date off by one day timezone JavaScript", you have met this bug. It is one of the most common defects in web software, and it has one root cause that shows up in many places. This post explains the cause, where it appears, how to store dates so it cannot happen, and how to test for it.

What actually goes wrong

A birthday, a due date, a holiday and an event date are all the same kind of value: a calendar date. They have a year, a month and a day. They do not have a time of day, and they do not belong to a timezone. 15 March is 15 March in Tokyo and in New York.

A JavaScript Date object is a different thing. MDN describes it as a single moment in time, stored as the number of milliseconds since midnight at the start of 1 January 1970, UTC. It is a timestamp. It has no calendar-date-only mode.

So when you turn a plain date into a Date object, you are forced to invent a time of day and a timezone. That invention is where the bug starts.

The parsing rule that surprises people

Take the string 2026-03-15. This is the ISO 8601 date-only format. MDN documents that when a string has no timezone offset, date-only forms are interpreted as UTC, while date-time forms (for example 2026-03-15T00:00:00) are interpreted as local time. MDN also notes that the UTC reading of date-only strings comes from a historical spec error that was not consistent with ISO 8601 but could not be changed because of web compatibility.

Two strings that look almost the same behave differently:

  • The date-only string 2026-03-15 becomes midnight UTC on 15 March.
  • The date-time string 2026-03-15T00:00:00 becomes midnight in the local timezone of the machine running the code.

Now the second half of the bug. Most display methods, such as toString, toLocaleDateString and the getDate family, convert the timestamp into the viewer's local timezone. If the viewer is west of UTC, for example in the United States, midnight UTC on 15 March is still the evening of 14 March on their clock. The page shows 14 March.

The chain is short: a date-only string is read as UTC midnight, then shown in local time, and anyone behind UTC sees the previous day. Anyone ahead of UTC usually sees the right day, which is why the bug often survives testing on the developer's own machine and only appears for a customer in another region.

Where the bug hides

The parsing example is the obvious case. In real systems the same mistake appears in quieter places.

  • APIs. A backend sends a date as a full timestamp string such as 2026-03-15T00:00:00Z when the field is really just a date. Every client then has to guess what to do with the time part.
  • Databases. A date stored in a timestamp column, or a date read through a driver that returns a Date object, picks up a time and a timezone along the way. A date column read into a language-level timestamp brings the problem with it.
  • Forms. A date picker returns a string, a component converts it to a Date, and a validation or formatting step converts it back. Each conversion is a chance to shift by a day.
  • Exports and imports. Spreadsheets and CSV files often carry dates with no timezone information. A tool that assumes UTC or local time on import will disagree with the tool that wrote the file.
  • Scheduled jobs and reports. A job that asks "which orders were placed today" depends on whose today it is. The server's day and the customer's day can differ.

The common thread is a date-only value passing through a timestamp on the way from one place to another.

How to store and send date-only values

The fix is a rule, not a library: if the value is a calendar date, keep it as a calendar date everywhere.

Use a date-only type in storage. Most databases have a date type separate from the timestamp type. Use it for birthdays, due dates and anything else that has no time of day. Do not use a timestamp column and hope the time part stays at midnight.

Send an ISO date string over the wire. The format is year, month, day with dashes, such as 2026-03-15. Do not add a time part and do not add a Z. In your API documentation, state that the field is a calendar date with no timezone.

Do not convert through a timestamp. Avoid building a Date object just to format, compare or add to a date-only value. Compare the strings, since ISO dates sort correctly as text. When you need to do arithmetic, such as adding 30 days to a due date, use a date library or the Temporal API that works on calendar dates directly. MDN lists Temporal as the newer API and marks Date as a legacy feature, so check browser support for your audience before relying on it.

Store a timezone only when a moment matters. A meeting at 3 pm, a payment that cleared, a login event: these are moments in time. Store them as a full timestamp, usually in UTC, and keep the user's timezone name if you need to show or schedule in their local time. Keep the two ideas apart in your data model: a calendar date has no zone, and a moment always does.

Be careful with day arithmetic. Adding 24 hours is not the same as adding one day. On days when clocks change for daylight saving, a local day is usually 23 or 25 hours long. Add days to a calendar date, not hours to a timestamp.

How to test for it

This bug is easy to write and easy to miss, because tests pass on a machine in one timezone. A few habits catch it.

Run the test suite in several timezones. Set the timezone of the test process explicitly, and run the same tests under at least one zone behind UTC, one ahead of UTC, and UTC itself. A test that passes in all three is far more trustworthy than one that passes on a laptop.

Test the boundary cases. Use dates near the edges: the first and last day of a month, 29 February in a leap year, and 31 December to 1 January. Use times of day close to midnight, both just before and just after.

Test around daylight saving changes. Pick the dates in your target regions when clocks move forward and back. Check that adding one day to a calendar date lands on the next calendar date, and that a due date on a change day still shows the right day.

Round-trip the value. Save a date, load it, send it through the API, render it, export it and import it again. The date you get at the end should be the date you started with, in every timezone you test.

Small data-type decisions like these are cheap on day one and costly to unwind after launch, when thousands of records already hold shifted dates. If you want senior engineers to review how your product handles dates, or to build it right from the start, see our web development services or book a call.

Frequently Asked Questions

Free · no obligation

Need this built?

Book a free 30-minute call. We'll discuss your goals, give you honest advice, and a clear estimate — no obligation.

Related services

Ways we can help

Keep reading

Related articles