TLDR: We stored event times as naive wall-clock UTC, then almost "fixed" the display — which would have shifted every event four hours wrong. The rot was in the write path, not the renderer.
The Setup
We were building an internal webinar event-management tool for an ecommerce client, replacing a Notion setup.
Events, milestones, tasks. One of the fields: the event's scheduled time.
Simple, right?
Famous last words.
The Bug Report
The handoff brief said: "Event times render in UTC instead of ET — Vercel runs UTC, so toLocaleString shifts the display. Just add timeZone: 'America/New_York' to the list formatter."
Totally reasonable.
One-line fix, ship it.
What I Checked First
Before touching anything, I opened both screens side by side: the events list (showing 8:09 AM) and the detail editor (also showing 8:09 AM).
Both agreed perfectly.
So I ran the math: if the stored value is 08:09Z and I add the ET formatter, what does the list render?
4:09 AM.
That's four hours off — and it would now disagree with the editor, which pulls the same raw value and was working fine.
Something was wrong with the diagnosis.
The Actual Problem
Here's what was really happening on the write side.
When a user typed 8:09 AM into a datetime-local input and hit save, the code did this:
new Date("2026-06-05T08:09").toISOString()
On Vercel — a UTC server — that parses 08:09 as UTC midnight + 8h 9m. It stores 08:09Z.
So the display wasn't broken. It was faithfully echoing what was stored.
The PROBLEM was that 08:09Z is 4:09 AM Eastern. An 8:09 AM Eastern event should be stored as 12:09Z (summer/EDT, UTC−4) or 13:09Z (winter/EST, UTC−5). Instead we committed the wall-clock number the user typed, dressed up in UTC clothing.
Nobody caught it because both reads agreed. Everything looked consistent. The data was just silently wrong.
(This is the most dangerous class of bug — the one that never throws, never mismatches, and only surfaces when someone asks "wait, did that webinar really start at 4 AM?")
The Fix That Actually Worked
At save time, parse the user's typed datetime as an ET instant, not UTC. The reliable way: use a timezone-name-based conversion rather than hardcoding an offset (hardcoding -04:00 is the DST trap restated — it's wrong half the year). Something like:
// Convert the user's "local" string to a true ET UTC instant
// using the timezone name so DST is handled automatically
const etFormatter = new Intl.DateTimeFormat('en-CA', {
timeZone: 'America/New_York',
// …then reconstruct as ISO with the right offset
})
Then display it back with an explicit ET formatter everywhere it renders:
new Date(storedUtcString).toLocaleString("en-US", { timeZone: "America/New_York" })
Round-trip is clean. Store the true moment. Display in the user's timezone. No drift, no silent wrongness.
(We also flagged old rows in the handoff — events saved under the naive pattern would shift on re-display once we fixed the formatter. Retroactive migration was out of scope, but we noted it clearly so no one got surprised.)
Why This Matters
The "fix" I almost shipped would have passed every eyeball test during dev — because I never would have asked whether the stored value was semantically correct. I would have just adjusted the display until it matched my expectation.
Empirical check before the patch. Always.
Open the write form. Open the read view. Compare what the DB actually holds to what the user intended. If they don't match, that's where the bug lives — not in the formatter.
P.S. The audit log in the same app was a display bug —
created_atis a true UTC instant that genuinely needed the ET formatter. Two fields, same symptom, completely different causes. Check both.