skip to content

A web app persists the user's unsaved draft in a `beforeunload` handler, with an `unload` handler as a backup, yet mobile users still lose drafts. Why are those two events unreliable, and which lifecycle event is the dependable place to save?

level: seniorimportance: must knowfreq 58%

answer

  1. pages stop more often than they exit
  2. no navigation means no unload
  3. the last guaranteed event
  4. dirty flag, small write
  5. warn with one, save with another

basics

~20 s

Mobile operating systems can kill a backgrounded tab without ever running unload or beforeunload, so neither is guaranteed. Save on the visibilitychange transition to hidden — the last event browsers reliably deliver — with pagehide as the secondary signal.

solid answer

~50 s

`unload` and `beforeunload` only fire on paths where the page is actually being torn down by a navigation the browser controls. On mobile the common exit is not a navigation at all: the user switches apps, the tab is backgrounded, and the operating system reclaims the process later with no further JavaScript executed. Neither handler runs, and the draft is gone. `beforeunload` has extra baggage — it exists to show a confirmation prompt, browsers ignore any custom message, and it needs prior user interaction to display anything; registering an `unload` handler also costs the page eligibility for the browser's back/forward restore. The durable save point is the `visibilitychange` transition to `"hidden"`, which fires on tab switch, app switch, minimise and screen lock, plus `pagehide` as a secondary signal for actual teardown. Keep the write small, synchronous-safe and idempotent, because you may run it many times per session and the page may be frozen immediately afterwards.

go deeper

for a junior

Know that beforeunload and unload are not guaranteed to run, especially on phones, and that visibilitychange to hidden is the event teams rely on for saving. Being able to name the right event is enough at this level.

for a middle

Explain the mechanism: a backgrounded tab can be killed with no navigation, so no unload-family event fires; describe beforeunload's real job as a confirmation prompt and its synchronous-only nature.

for a senior

Show the production shape end to end — dirty flag, small synchronous write on hidden, pagehide as a secondary signal, restore-and-reconcile on startup — and name the back/forward restore cost of an unload handler.

for a principal

Define the durability contract for the product: what state must survive an abrupt kill, what may be reconstructed, where the authority lives between client and server, and how features are reviewed so each one does not invent its own save-on-exit scheme.

## The mental model that causes the bug Developers picture a page ending the way a desktop program ends: the user closes it, an exit hook runs, cleanup happens. `beforeunload` and `unload` encode that picture. But on the modern web — and overwhelmingly on mobile — a page far more often *stops* than *exits*: the user swipes to another app, locks the phone, or switches tabs, and the page sits in the background until the operating system decides it wants the memory back. When that happens the process disappears. There is no navigation, no teardown sequence, and no opportunity for your handler to run. That is the whole answer to "why did the draft vanish": the code that would have saved it was scheduled for an event that never occurred. ## Taking each event in turn **`unload`.** Fires, when it fires at all, as the document is being discarded during a navigation. It is skipped entirely when a tab is killed while backgrounded, and it is unreliable in practice on mobile even for ordinary navigations. It is also actively harmful: registering an `unload` listener disqualifies the page from the browser's back/forward restore optimisation, so a handler added "just in case" makes back navigation measurably slower for every user. Modern guidance is to not use it at all. **`beforeunload`.** Its real purpose is narrow: give the user a chance to cancel a navigation that would lose work. Calling `event.preventDefault()` inside it asks the browser to show a confirmation dialog — the browser's own generic wording; custom strings have been ignored for years — and browsers only show it if the user has interacted with the page (sticky activation). It has the same fundamental hole as `unload`: no navigation, no event. And it is synchronous-only; an `await`ed save inside it will not complete, because the browser does not wait for your promise. **`visibilitychange` to `"hidden"`.** This fires on every path by which the user stops looking at the page: switching tabs, switching apps, minimising, locking the screen, and also as part of navigating away. It is delivered *before* the page is eligible to be frozen or killed. That makes it the last moment you can count on running code — which is exactly the definition of a durable save point. **`pagehide`.** Fires when the document is being torn down or set aside on a navigation. Useful as a secondary signal for teardown-shaped cleanup, and far better behaved than `unload`, but it does not cover the app-switch case that visibility does. ## The shape of a correct save ```js let dirty = false; function markDirty() { dirty = true; } function persistDraft() { if (!dirty) return; localStorage.setItem('draft', JSON.stringify(collectDraft())); dirty = false; } document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') persistDraft(); }); window.addEventListener('pagehide', persistDraft); ``` Four properties matter. **Idempotent.** This now runs on every tab switch, dozens of times a session. A dirty flag keeps it cheap and lets the handler be attached twice without harm. **Small and fast.** You are running at a moment the browser is transitioning; a large serialisation blocks it. Keep the payload minimal and prepare it incrementally if it is expensive. **Synchronous-safe.** Anything asynchronous may not finish, because the page can be frozen or killed immediately after the handler returns. A save whose completion depends on a promise resolving after the transition is a save you cannot rely on. **Restored deliberately.** Saving is half the feature. On startup, read the stored draft, and decide the reconciliation rule — offer to restore, merge, or discard on successful submit — so a stale draft does not resurface after the user has already sent it. ## Where `beforeunload` still belongs One place: warning the user before they lose work, registered only while there is unsaved work and removed the instant there is not. A permanently-installed `beforeunload` is a usability bug — it prompts on every exit and offers no information. Use it for the prompt, and never as your persistence mechanism. ## What interviewers listen for The candidate who names `beforeunload` and stops is at the bottom of the range. The candidate who explains that mobile pages are killed while backgrounded, that no unload-family event covers that path, and that the hidden transition is therefore the real save point, is demonstrating exactly the production experience the question is probing. Adding that `unload` handlers cost back/forward restore eligibility, and that the save must be small, idempotent and synchronous-safe, is what separates a good answer from a complete one.

  • What does calling event.preventDefault() inside a beforeunload handler actually do today?
    It asks the browser to show its own generic confirmation dialog before leaving. Custom message strings have been ignored for years, and browsers only show the prompt when the user has already interacted with the page, so an untouched page navigates away silently. It cancels nothing by itself and never waits for asynchronous work.
  • Why is registering an unload handler harmful even when it appears to work?
    Beyond being unreliable, an `unload` listener disqualifies the page from the browser's back/forward restore path, so every back navigation pays a full reload instead of an instant restore. You take a real, measurable regression for every user in exchange for a handler that will not fire in the case you added it for.
  • You now save on every hidden transition. How do you keep that from becoming expensive?
    Track a dirty flag and return immediately when nothing changed, keep the serialised payload small, and prepare it incrementally as the user edits rather than building it inside the handler. The write should be a short synchronous operation — anything awaited may never complete, because the page can be frozen right after the handler returns.
  • Should you still use beforeunload at all?
    Only to warn. Register it while there is genuinely unsaved work and remove it the moment there is not, so the user gets a prompt exactly when leaving would lose something. It should never be your persistence mechanism — the save has already happened on the hidden transition by the time any prompt appears.

saying these in an interview costs you the question

  • Names beforeunload as the reliable place to save
  • Assumes unload always runs when a tab closes
  • Puts an awaited async save inside beforeunload
  • Believes a custom beforeunload message is shown to the user
  • Leaves a beforeunload handler installed permanently

context