Debugging 2026-05-14

Safari-Only Bugs: a Debugging Guide for the WebKit-Weary

Here’s a claim I’ll defend: if a bug report says “works in Chrome, broken in Safari,” you can guess the cause correctly about half the time before opening a single devtool, because Safari-only bugs cluster hard around the same five behaviors. When you need to debug an issue that only happens in Safari, you’re rarely facing some exotic WebKit rendering defect. You’re facing a short list of deliberate design decisions — stricter parsing, aggressive privacy limits, an opinionated cache — that Chrome happens to be more forgiving about.

I’ve shipped frontends long enough to have filed WebKit bugs that got closed as “behaves as intended,” and honestly, they usually did behave as intended. The intent was just different from Chrome’s. Here are the greatest hits, with detection code, and then how to actually get a debugger attached when you don’t own any Apple hardware.

The greatest hits

Date parsing strictness

The classic. Your API returns "2026-05-14 09:30:00", and:

new Date("2026-05-14 09:30:00")
// Chrome:  Wed May 14 2026 09:30:00 — shrugs, parses it
// Safari:  Invalid Date

The ECMAScript spec only guarantees parsing for ISO 8601 (2026-05-14T09:30:00, with the T). Everything else is implementation-defined, and Chrome chose to be a doormat about it while Safari chose not to be. The downstream symptom is never “Invalid Date” on screen, of course — it’s NaN propagating through a date-diff, a chart with no x-axis, a “your trial expired NaN days ago” email. Detection is trivial once you suspect it:

const d = new Date(apiValue)
if (Number.isNaN(d.getTime())) {
  console.error('Unparseable date:', apiValue)
}

Fix: emit strict ISO 8601 from your backend, or parse explicitly with a library instead of trusting new Date(string) with anything a human or a database typed. I treat any non-ISO date string crossing a service boundary as a bug regardless of whether Chrome tolerates it.

Intelligent Tracking Prevention and disappearing storage

ITP is Safari’s anti-tracking machinery, and it’s the reason “the user keeps getting logged out, but only on their iPhone” exists as a ticket genre. The two behaviors that bite app developers: third-party cookies are blocked outright, and — the one that genuinely surprises people — script-set storage (cookies set via document.cookie, and in some circumstances localStorage) can be capped or purged after roughly seven days without user interaction with your site.

So the user who visits weekly is fine; the user who comes back after a two-week vacation is mysteriously signed out, and only on Safari. Nothing in your code is wrong. You cannot detect the purge before it happens, only design around it: keep authentication in HttpOnly cookies set by your server (server-set cookies aren’t subject to the seven-day script-storage cap), and treat localStorage as a cache, never as the system of record for anything you’d be sad to lose.

If your app embeds third-party widgets that need their own storage, test them in Safari specifically — the widget vendors’ docs are written against Chrome’s behavior more often than anyone admits.

100vh and the iOS toolbar

On iOS Safari, the browser toolbar collapses as you scroll, which means the viewport height changes during scrolling. 100vh is defined as the largest viewport — toolbar collapsed — so a height: 100vh element is taller than the visible screen when the toolbar is showing, and your bottom-pinned CTA sits underneath Safari’s chrome. Every mobile web developer has shipped this bug at least once. I’ve shipped it twice, years apart, which tells you something about me.

The modern fix is the new viewport units, which WebKit itself championed:

.fullscreen {
  height: 100vh;  /* fallback for older browsers */
  height: 100dvh; /* tracks the toolbar as it collapses/expands */
}

svh is the smallest viewport (toolbar visible), lvh the largest, dvh dynamically follows the current state. For anything interactive pinned to the bottom edge, dvh or svh. These have been supported in all major browsers for a while now; the vh fallback is for the long tail.

IndexedDB in private mode

Safari’s private browsing has a history of treating storage APIs unusually. In older versions, localStorage existed but had a quota of zero, so writes threw QuotaExceededError; IndexedDB has been variously unavailable, erroring on open, or wiped per-session. Modern Safari is better behaved, but “the app crashes only in private tabs, only on Safari” still traces back to storage assumptions often enough that I check it early.

The defensive pattern — probe, don’t assume:

async function storageWorks() {
  try {
    localStorage.setItem('__probe__', '1')
    localStorage.removeItem('__probe__')
    const db = await new Promise((resolve, reject) => {
      const req = indexedDB.open('__probe__')
      req.onsuccess = () => resolve(req.result)
      req.onerror = () => reject(req.error)
    })
    db.close()
    return true
  } catch {
    return false
  }
}

Run it at startup and degrade to in-memory storage when it fails. Your app should have an answer to “what if I can’t persist anything” anyway; Safari private mode is just the environment that grades that homework.

The back-forward cache eating your JavaScript state

Safari’s bfcache is aggressive: navigate away and hit the back button, and Safari restores the page as a frozen snapshot — DOM, JS heap, timers, everything — without firing load or re-running your scripts. Wonderful for perceived performance. Less wonderful when your page resumes with a stale auth token, a WebSocket it believes is open, or a “submitting…” spinner from a form that finished a navigation ago.

The hook you need is pageshow:

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    // Restored from bfcache: load never fired, state is frozen-in-time
    revalidateSession()
    reconnectSockets()
  }
})

event.persisted is true only for bfcache restores. Chrome and Firefox have bfcaches too, but Safari’s has historically been the most eager, which is why this class of bug wears a Safari costume even though the fix is universal.

How to debug an issue that only happens in Safari

Knowing the suspects is half the job. The other half is getting eyes on the actual failure, which is harder when the browser in question is welded to an operating system you may not run.

If you have a Mac, Safari’s Web Inspector attaches to iOS devices over USB (enable Web Inspector in the iPhone’s Safari settings, then Develop menu on the Mac) — and the iOS Simulator in Xcode runs real WebKit, which covers most rendering and JS behavior without hardware.

If you don’t own a Mac: cloud device farms (BrowserStack, LambdaTest, and similar) give you real Safari on real Apple hardware by the minute, which beats guessing. For quick-and-dirty console access on a colleague’s iPhone, remote-inspector tools like inspect builds or Eruda (a devtools panel injected into the page itself) are undignified but effective. Note that Chrome and Firefox on iOS run WebKit under the hood, so “it also breaks in Chrome on my iPhone” doesn’t rule Safari-the-engine out — that’s still WebKit wearing a trench coat. (This is slowly changing with the EU’s alternative-engine rules, but WebKit remains what your iOS users are running.)

And instrument for the case where you can’t reproduce at all. This category of bug is where session replay pays for itself: filter your errors or replays to Safari-only, and the pattern usually announces itself — every affected session is iOS, or every one is private browsing, or every user was returning via the back button. I’ve closed WebKit-flavored mysteries in an afternoon this way that would’ve been a week of “cannot reproduce” ping-pong, because watching one affected session replaces guessing at ten. Safari-only bugs are a subspecies of the broader genus — bugs that only happen to specific users — and the same triage discipline applies: segment first, theorize second.

Safari isn’t the new IE, whatever the memes say. IE was wrong and stagnant; Safari is opinionated and moving. But opinionated engines produce opinionated bugs, and the five above will cover most of the tickets with “only in Safari” in the title. Check the dates first. It’s always the dates.

See the bug the way your user did

LogReplay captures session replays, console output, network requests, and errors in one timeline — so you stop guessing what happened before the ticket arrived.

Try LogReplay free