Safari shows Invalid Date when parsing a non-ISO datetime string
Event countdowns showed NaN days for iPhone users while Chrome was fine. Our API sent dates like 2024-12-3 9:05:00, which only some engines accept. We switched to strict ISO 8601 with an offset and stopped trusting Date parsing.
We run ticketing for regional music festivals. Each event page shows a countdown, "Doors open in 3 days 4 hours". In late November a promoter forwarded a screenshot from her iPhone: "Doors open in NaN days". Our Sentry project had the matching noise, all from Safari on iOS and macOS:
RangeError: Invalid time value
at format (assets/vendor-dates.js:1:2201)
at Countdown (assets/EventPage.js:1:884)
Chrome, Edge and Firefox users on the same pages saw correct countdowns. The error started on November 22, the day our backend team shipped a refactor of the events API.
Reproducing it
The failing value came straight from the API response:
{ "id": 4812, "name": "Harbour Lights", "doors_open": "2024-12-6 9:05:00" }
In Safari's console:
new Date('2024-12-6 9:05:00'); // Invalid Date
new Date('2024-12-06 09:05:00'); // Fri Dec 06 2024 09:05:00 (local time)
new Date('2024-12-06T09:05:00+07:00'); // valid everywhere
In Chrome, all three lines produce a valid date.
What we ruled out
- The space between date and time. This was everyone's first guess, because older Safari
famously rejected
YYYY-MM-DD HH:mm:ss. WebKit relaxed that in early 2022, and current Safari accepts it, as the second line above shows. It was still a factor for a small number of users on old iOS versions, which we come back to below. - Timezone handling in our formatting library. The library threw because it received an
Invalid Date, not because of a timezone problem. - A Safari-specific polyfill. We do not ship one, and the failure reproduced in a plain console.
Root cause
The API refactor replaced a database column formatter with PHP's date('Y-n-j G:i:s'). The
n, j and G format characters produce month, day and hour without leading zeros, so early-month
dates and morning times came out as 2024-12-6 9:05:00. That is not in the ECMAScript date time
string format. Chrome's and Firefox's fallback parsers guess and accept it; Safari's JavaScriptCore
returns Invalid Date. Dates from the 10th to the 31st with times after 10:00 still worked, which is
why the bug looked intermittent.
What is happening under the hood
ECMAScript defines exactly one string format that every engine must parse: a simplified ISO 8601
form, YYYY-MM-DDTHH:mm:ss.sssZ, with optional parts and either Z or a ±HH:mm offset. For any
other string, the specification says the engine may fall back to "implementation-specific
heuristics". Each engine has its own heuristics:
| Input | Chrome / Firefox | Safari 18 | Notes |
|---|---|---|---|
2024-12-06T09:05:00+07:00 |
valid | valid | Spec format, unambiguous |
2024-12-06T09:05:00 |
valid, local time | valid, local time | Spec format, no offset |
2024-12-06 09:05:00 |
valid | valid | Accepted by WebKit since 2022 |
2024-12-6 9:05:00 |
valid | Invalid Date | Single-digit parts |
Two details make this worse. First, date-only ISO strings like 2024-12-06 are parsed as UTC, while
date-time strings without an offset are parsed as local time, so even "valid" strings can shift by a
day. Second, a missing offset means the instant depends on the user's device timezone. Our festival
in Ho Chi Minh City would have shown a different countdown to someone checking from Sydney even if
parsing had worked.
The single-digit failure is tracked in WebKit's bug tracker. The space separator was relaxed in WebKit in January 2022 and reached users with the Safari releases that followed. Our Sentry data showed about 3 percent of affected sessions came from early iOS 15 builds, where even the zero-padded version with a space fails.
Environment
| Component | Version / value |
|---|---|
| Browsers failing | Safari 18.1 on macOS 15.1, Safari on iOS 18.1, a few early iOS 15 WebViews |
| Browsers passing | Chrome 131, Edge 131, Firefox 133 |
| Frontend | Vue 3.5 with date-fns 4.1 |
| Backend | PHP 8.3, Laravel 11.33 API |
| Data source | MySQL 8.0 DATETIME column, venue timezone stored separately |
Minimal reproduction
// Open in Safari and Chrome and compare
const inputs = ['2024-12-6 9:05:00', '2024-12-06 09:05:00', '2024-12-06T09:05:00+07:00'];
for (const s of inputs) {
const d = new Date(s);
console.log(s, Number.isNaN(d.getTime()) ? 'Invalid Date' : d.toISOString());
}
Who is affected
Any frontend that passes server-provided strings into new Date() or Date.parse() without
controlling the format. Common sources are SQL DATETIME values serialized as-is, PHP or Python
format strings without zero padding, and locale-formatted dates like 12/6/2024. All browsers on iOS
use WebKit's engine, so "Chrome on iPhone" fails too.
The fix
Backend: send real ISO 8601 with an offset. The venue's timezone is the meaningful one, so we serialize the instant in UTC plus the IANA zone name separately:
// app/Http/Resources/EventResource.php (excerpt)
'doors_open' => $this->doors_open->clone()->utc()->toIso8601ZuluString(), // 2024-12-06T02:05:00Z
'timezone' => $this->venue->timezone, // Asia/Ho_Chi_Minh
toIso8601ZuluString() is Carbon's helper that always produces zero-padded fields with a Z
suffix. We also stopped formatting dates by hand anywhere in API resources.
Frontend: parse explicitly, never with the bare constructor. We added one parsing function and made it reject anything that is not the format we agreed on:
// src/lib/dates.ts
import { parseISO, isValid } from 'date-fns';
const ISO_WITH_OFFSET = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})$/;
export function parseApiDate(value: string): Date {
if (!ISO_WITH_OFFSET.test(value)) {
throw new Error(`Unexpected date format from API: ${value}`);
}
const d = parseISO(value);
if (!isValid(d)) throw new Error(`Unparseable date: ${value}`);
return d;
}
parseISO from date-fns parses ISO 8601 itself instead of delegating to the engine, so the result is
identical in every browser. The regex is deliberately strict: we would rather see a clear error in
Sentry than a silent NaN in the UI.
The countdown then formats in the venue's zone with Intl.DateTimeFormat and timeZone: event.timezone, so everyone sees "Doors open 9:05 AM (Ho Chi Minh City time)".
Defence for old data. Cached API responses and a partner feed still carried the old format for a
few days, so we added a narrow legacy parser that zero-pads Y-n-j G:i:s before parsing and logs
every use:
const LEGACY = /^(\d{4})-(\d{1,2})-(\d{1,2}) (\d{1,2}):(\d{2}):(\d{2})$/;
export function parseLegacyVenueTime(value: string, offset: string): Date {
const m = LEGACY.exec(value);
if (!m) throw new Error(`Not a legacy datetime: ${value}`);
const [, y, mo, d, h, mi, s] = m;
const pad = (x: string) => x.padStart(2, '0');
return parseApiDate(`${y}-${pad(mo)}-${pad(d)}T${pad(h)}:${mi}:${s}${offset}`);
}
Alternatives and trade-offs
- Replace the space with
Ton the client. A popular one-line fix, but it does nothing for single-digit fields and still yields a local-time interpretation. - Send epoch milliseconds. Unambiguous and easy to parse, but unreadable in logs and you still need the venue timezone for display.
- Temporal.
Temporal.Instant.from()andTemporal.ZonedDateTimeare the long-term answer to these problems, but no stable browser ships Temporal yet, so it means a polyfill. We will revisit.
How to test the fix
Unit tests run in Node, which uses V8 and would accept the bad strings, so the important test is that our parser rejects them regardless of engine:
import { describe, expect, it } from 'vitest';
import { parseApiDate } from './dates';
describe('parseApiDate', () => {
it('accepts ISO 8601 with offset', () => {
expect(parseApiDate('2024-12-06T02:05:00Z').toISOString()).toBe('2024-12-06T02:05:00.000Z');
});
it.each(['2024-12-6 9:05:00', '2024-12-06 09:05:00', '2024-12-06T09:05:00', '12/6/2024'])(
'rejects %s',
(input) => expect(() => parseApiDate(input)).toThrow(),
);
});
We also added WebKit to our Playwright projects so the event page is rendered by a real WebKit build in CI:
npx playwright test --project=webkit tests/event-page.spec.ts
Confirming in production
- The Sentry issue for
RangeError: Invalid time valuewent from about 700 events per day to zero within two hours of the frontend deploy; the backend fix had already removed new occurrences except from cached responses. - The legacy parser logged 214 uses in the first 24 hours and none after the API cache expired, after which we removed it.
- We spot-checked event pages on an iPhone running iOS 18.1 and on BrowserStack with iOS 15.2: correct countdowns on both.
Regression guard
- The API has a contract test asserting every
*_atanddoors_openfield matches the ISO 8601 with offset pattern. - An ESLint
no-restricted-syntaxrule flagsnew Date(<string variable>)andDate.parse()in application code, pointing toparseApiDate. - The WebKit Playwright project runs on every pull request.
Security implications
None beyond the bug itself; no data was exposed. Strict parsing slightly reduces risk, because lenient parsers accept surprising inputs that can bypass date-based checks, for example a "valid until" date entered in an unexpected format. Never rely on client-side date parsing for authorization or pricing decisions; the server must evaluate time-based rules itself.
Prevention checklist
- Serialize instants as ISO 8601 with
Zor an explicit offset, always zero-padded. - Send the IANA timezone separately when local wall time matters to the user.
- Never pass arbitrary strings to
new Date(); use one strict parsing function. - Run at least one CI browser project on WebKit.
- Add contract tests on date formats when backend and frontend are owned by different teams.
References
- ECMAScript specification: Date Time String Format — https://tc39.es/ecma262/#sec-date-time-string-format
- MDN:
Date.parse()— https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse - WebKit bug 283825: non standard
new Date('2024-12-3')yields Invalid Date — https://bugs.webkit.org/show_bug.cgi?id=283825 - WebKit bug 235468: Relax Date.parse requirement — https://bugs.webkit.org/show_bug.cgi?id=235468
- date-fns
parseISO— https://date-fns.org/docs/parseISO - TC39 Temporal proposal — https://tc39.es/proposal-temporal/docs/
Add a label
Embed this post
<iframe src="https://hibug.me/safari-invalid-date-parsing-datetime-string" width="700" height="420" frameborder="0" loading="lazy"></iframe>
I have spent most of my career on the side of software that pages people at night: payment integrations, web platforms with real traffic, CI/CD pipelines, Kubernetes clusters and the databases underneath them. Somewhere along the way I started keeping a notebook of every bug that cost more than an hour — the exact error, what we ruled out, the real root cause and how we proved the fix. Hibug is where those notes live. Every post follows the same path: Investigate, Explain, Fix, Verify — with the environment spelled out, the fix tested, and the security angle checked.
Related Stories
Customers in Japan and Korea charged 100x: zero-decimal currencies multiplied by 100
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.