What this usually means
JavaScript's `Date` object is always a moment in UTC internally. When you parse a date string without a timezone, JavaScript guesses whether to treat it as UTC or local time — and the guess depends on the string format. ISO 8601 dates like `'2025-03-15'` are treated as UTC midnight. But if your server or database expects local time, the date shifts by your UTC offset. Similarly, displaying a UTC date in a local timezone can push it across midnight into the next or previous day.
The first ten minutes \u2014 establish facts before touching code.
- 1Check the exact string being parsed. `'2025-03-15'` (ISO date-only) is parsed as UTC. `'2025-03-15T00:00:00'` is also UTC. `'03/15/2025'` is parsed as local time.
- 2Check the database column type. `DATE` (no time) vs `TIMESTAMP` (with time) vs `TIMESTAMPTZ` (with timezone). Storing a UTC midnight timestamp in a DATE column can truncate to the wrong day.
- 3Log the raw Date value at each step: after parsing, before saving, after reading, before displaying. Find which step introduces the shift.
- 4Check the server's timezone (`process.env.TZ`, system timezone). If it is different from the user's timezone, date display can shift.
- 5Verify the API serialises dates consistently. JSON has no native date type — dates are usually strings. The format matters.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchDate parsing code — how the input string becomes a Date object
- searchDatabase schema — column types for date fields (DATE vs TIMESTAMP vs TIMESTAMPTZ)
- searchAPI serialisation — JSON.stringify or custom date formatting
- searchFrontend date display — `toLocaleDateString`, date-fns, dayjs, moment formatting
- searchServer timezone configuration — `TZ` env var, system locale
- searchORM date handling — how your ORM maps database date types to JavaScript Date objects
Practical causes, not theory. These are the things you will actually find.
- warningParsing `'2025-03-15'` as UTC midnight, then formatting in a negative-offset timezone shifts to the previous day
- warningStoring a JavaScript Date as a `DATE` column — the database truncates the time, potentially shifting the day
- warningThe server timezone differs from the user's timezone, and the display code uses local time
- warningJSON serialisation drops the timezone offset, and the consumer parses it as local time
- warningDaylight Saving Time transition — one day has 23 or 25 hours, shifting midnight calculations
- warningUsing `new Date(dateString)` with inconsistent date string formats across the codebase
Concrete fix directions. Pick the one that matches your root cause.
- buildParse dates with an explicit format: use a library like date-fns `parseISO` or dayjs with a format string
- buildStore dates as UTC and convert to the user's timezone only for display
- buildUse `TIMESTAMPTZ` in the database so timezone is preserved with the value
- buildSerialise dates as ISO 8601 with timezone offset: `'2025-03-15T00:00:00.000Z'`
- buildUse date-only strings (`'2025-03-15'`) without time for date-only fields and never convert them to Date objects that include time
A fix you cannot prove is a guess. Close the loop.
- verifiedEnter a date in the UI, submit, reload, and confirm the displayed date matches the input.
- verifiedTest with a user timezone that is UTC+14 (Kiritimati) and UTC-12 (Baker Island) — both edge cases.
- verifiedTest around a DST transition date for the user's timezone.
- verifiedWrite a unit test that parses a date string, serialises it, and asserts the round-trip preserves the date.
- verifiedCheck the database directly: `SELECT date_column FROM table WHERE id = ...` and compare to the input.
Things that make this bug worse or harder to find.
- warningUsing `new Date()` without understanding its parsing rules
- warningAssuming all users are in the same timezone as the server
- warningStoring local time in the database instead of UTC
- warningUsing `toISOString()` for display — it always outputs UTC, which may not match user expectation
- warningNot testing date logic across timezone and DST boundaries