Dates shift by a day for some users
“A due date saved as March 15 shows as March 14 for users in the Americas, but looks right for us and in tests. Moving the server or changing DST makes it worse.”
THE PRINCIPLE
Decide per field whether it is a calendar date, a wall-clock time in a known zone, or an absolute instant. Only instants belong in a Date.
FIRST MOVES
- Reproduce with TZ=America/Los_Angeles (Node) or a DevTools sensor time zone override; most CI and servers run in UTC and hide the bug.
- Grep for new Date(someString) and Date.parse: date-only strings are read as UTC, date-time strings without an offset as local time.
- Keep calendar dates (birthdays, due dates, billing days) as YYYY-MM-DD strings or Temporal.PlainDate end to end, and a DATE column in the database; check how your driver maps DATE.
- For instants, store UTC with an offset (ISO with Z or timestamptz) and format with Intl.DateTimeFormat and an explicit timeZone.
- Check Temporal support before relying on it: Chrome 144+, Firefox 139+ and Node.js 26 ship it, but Safari does not yet, so it is not Baseline; use a polyfill elsewhere.
TOOLS: THEN → NOW
Date plus manual offset math, or Moment.jsTemporal (PlainDate, ZonedDateTime, Instant), with @js-temporal/polyfill or temporal-polyfill where not native
Formatting with toLocaleDateString() in the runtime's zoneIntl.DateTimeFormat with an explicit timeZone from the user's profile
PATTERN SNAPSHOT
new Date("2026-03-15"); // UTC midnight: Mar 14, 5pm in Los Angeles
new Date("2026-03-15T00:00"); // local midnight: depends on the runtime's zone
// Calendar date: no time zone involved at all.
const due = Temporal.PlainDate.from("2026-03-15");
due.toLocaleString("en-US", { dateStyle: "medium" }); // "Mar 15, 2026" everywhere
// Without Temporal: format a date-only value in UTC so it cannot shift.
new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeZone: "UTC" })
.format(new Date("2026-03-15"));
// Instant: stored as UTC, shown in the user's zone.
new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeStyle: "short", timeZone: user.timeZone })
.format(new Date(row.createdAt));CLOSE THE AI. EXPLAIN THIS.
Why does new Date("2026-03-15") and new Date("2026-03-15T00:00") give different instants, and which kind of value should a birthday be?RELATED SYMPTOMS
SOURCES
Guide reviewed