·6 min read
The cost of a bad date
The same timestamp string is a valid date in Chrome and an Invalid Date in Safari, and one method call turns that into a white screen.
- frontend
- snowflake
- safari
- bugs
I shipped a dashboard that worked on every laptop in the office and showed a white screen on every phone. The data was fine. The query was fine. One timestamp string, parsed by two engines, gave two different answers, and one of those answers throws.
One timestamp, straight out of the warehouse
Two characters are marked: a space where ISO 8601 wants a T, and a space before the Z.
Chromium · v148, probed
Valid
2026-10-06T14:30:00.000Z
WebKit · v26, probed
Invalid Date
RangeError: Invalid Date
Your browser · live
Not probed
evaluating
It looks like a date
Three parsers, two answers
Then it throws
A regex fixes it
Try your own
Where the string comes from
Snowflake, and most warehouses, serialise a TIMESTAMP_TZ as 2026-10-06 14:30:00.000 Z. That is a space between the date and the time, and a space before the zone. ISO 8601 wants a T in the first spot and nothing in the second. The database is not wrong. It is just not writing for new Date().
The spec for Date.parse only guarantees one format: the ISO 8601 profile. Anything else is up to the engine, and the engines disagree.
What the engines actually do
I did not want to argue this from memory, so I probed it. A script launches Chromium and WebKit headless through Playwright, evaluates new Date(s) for fourteen strings in each, and records whether the result is valid and what toISOString() returns. The playground above reads from that table.
12 of 14
strings Chromium accepts
Chromium 148, probed
9 of 14
strings WebKit accepts
WebKit 26.4, probed
3
strings only Chromium accepts
all three use a space separator or US dashes
The three strings Chromium accepts and WebKit rejects are the warehouse format with a space and a Z, the same with a +0000 offset, and 10-06-2026. Two strings fail everywhere: a T followed by a space before the Z, and an epoch as a string. Chromium is not lenient about everything. It is lenient about the exact shape a warehouse emits, which is why the bug hides on a developer’s machine.
The other trap in the table is the string with no zone at all. 2026-10-06 14:30:00 parses in both engines, but as local time. My probe ran in Denver, so both returned 20:30 UTC. Your reader in London gets a different instant from the same row. Valid is not the same as correct.
Why it is a white screen and not a wrong number
Invalid Date is a quiet value. You can format it, compare it, put it in state, and nothing complains. Then a component calls .toISOString() on it, which is what happens the moment you build a query param, a cache key, or an axis tick from that date. That method throws a RangeError.
A throw during render in React with no error boundary above it unmounts the tree. Not the tile. The whole app. On Chrome the same code path returns a string and carries on. So the bug reproduces on exactly the devices you did not test on, which for most dashboards means the phone the executive opens it on.
The fix
Normalise before you parse. Do not reach for a date library for this. The shape is fixed and a regex handles it.
const WAREHOUSE_TS =
/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2}(?:\.\d+)?) ?(Z|[+-]\d{2}:?\d{2})?$/;
export function toIso(s: string): string {
const m = WAREHOUSE_TS.exec(s.trim());
if (!m) return s; // not ours, leave it alone
const [, day, time, zone = 'Z'] = m; // no zone: treat as UTC
const offset = /^[+-]\d{4}$/.test(zone) ? `${zone.slice(0, 3)}:${zone.slice(3)}` : zone;
return `${day}T${time}${offset}`;
}
export function safeDate(s: string): Date | null {
const d = new Date(toIso(s));
return Number.isNaN(d.getTime()) ? null : d;
}
The probe ran this same function on every string. After it, WebKit accepts 12 of 14 and Chromium 13 of 14. The two that still fail are the ones the regex does not claim: the US dashes on WebKit, and the epoch string on both. That is the right behaviour. A normaliser that guesses at every format is a new source of bugs.
The zone = 'Z' default is an opinion. Warehouse timestamps without a zone are almost always UTC in my experience. If yours are wall-clock local, change the default, but change it on purpose.
What to do
- Grep your frontend for
new Date(and check what feeds each one. Anything that came from a warehouse or an API you do not own goes throughsafeDate. - Add an error boundary around every dashboard tile. A bad date should blank one tile, not the page.
- Put
safari14in your Vite build target and test on a real iPhone before a release. WebKit is the only engine allowed on iOS, so every browser there has the same parser. - Keep the probe script. Rerun it when engines update and diff the table. Parsers change quietly.
The dashboard that started this was fixed in one commit. Finding it took a week, because everything I had in front of me said the date was fine.
One visual essay, every week.
No roundups, no sponsors. Unsubscribe in one click.