·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.

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

2026-10-06 14:30:00.000 Z

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

This is what a TIMESTAMP_TZ column looks like when it lands in JSON: a space where ISO 8601 wants a T, and a space before the Z. Every warehouse driver I have used does something like it.

Three parsers, two answers

Chromium shrugs and parses it. WebKit, which is Safari and every browser on an iPhone, returns Invalid Date. Your own browser gets a vote too, evaluated live.

Then it throws

Invalid Date is a quiet failure until something calls toISOString() on it. That throws a RangeError, and a throw during render with no error boundary takes the whole React tree down.

A regex fixes it

Normalise before you parse. A regex turns the space into a T, drops the space before the zone, and defaults to UTC when the zone is missing. Both engines now agree.

Try your own

Pick any of the probed strings, or type one. Probed engines read from a table I generated with Playwright; the third card runs new Date() in the browser you are holding.

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

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.

ShareXBlueskyLinkedInEmail

One visual essay, every week.

No roundups, no sponsors. Unsubscribe in one click.