# Offline fallback for service workers

> How a service worker returns a precached page, image, or JSON body when a navigation or subresource request fails with nothing cached for its URL, which requests qualify, what Response.error() does, and the Workbox setCatchHandler equivalent.

An offline fallback is a response the service worker stores during `install` and returns from the `fetch` handler when the network request fails and Cache Storage has nothing for that URL, so the user sees an application page instead of the browser's error screen. It uses only `FetchEvent`, `cache.add()`, and `caches.match()`, which are Baseline widely available (Chrome 40, Firefox 44, Safari 11.1, Edge 17; BCD `api.FetchEvent`), so the pattern works identically wherever a service worker runs.

## How it works

A fallback is the last branch of a Network First or Cache First handler, not a strategy of its own. The order of checks determines what the user sees:

1. `fetch(event.request)` rejects. It rejects only on network failure (DNS, TCP, TLS, or an aborted connection); a `404` or `503` resolves, so a server error is not an offline condition and should be returned as-is.
2. `caches.match(event.request)` returns `undefined`, meaning this exact URL was not cached, or has since been evicted.
3. The handler returns the precached fallback for the request's type: `/offline.html` for `event.request.mode === 'navigate'`, a placeholder SVG for `event.request.destination === 'image'`, a synthetic JSON body for `/api/` paths.
4. For anything else the handler returns `Response.error()`, a network-error response with `type` `'error'` and `status` `0`, which makes the page's `fetch()` reject exactly as it would without a worker.

The `mode === 'navigate'` guard matters because the browser's own offline page is only a problem for navigations; returning HTML for a failed script or image request would be parsed as the wrong type. The fallback must be cached in `install` with `cache.add()` (or inside `addAll()`), so the install fails when the fallback URL is unavailable; a fallback fetched lazily would be missing exactly when it is needed. An `<a href>` on the fallback page to any URL not in the cache will fail in the same way, so the page should link only to precached routes or carry its own retry button that calls `location.reload()`.

Fallback HTML is served with the status the cached response carried (`200` for a precached file) unless the handler constructs a new `Response` with `status: 503`; the browser renders either, so the choice mainly affects server-side analytics that read status codes. The fallback page itself should not reference uncached scripts, stylesheets, or web fonts: each one fails with the same network error and the page renders unstyled.

:::observed
When a navigation fails with no service worker fallback, Chrome (English UI) shows its own interstitial titled `No internet` with the error code `ERR_INTERNET_DISCONNECTED`, and the DevTools Console records `GET https://example.com/ net::ERR_INTERNET_DISCONNECTED`; once a `fetch` handler returns a precached `/offline.html`, the interstitial no longer appears and no `net::ERR_*` line is logged for the navigation, although the worker's own failed `fetch()` still logs one. `ERR_INTERNET_DISCONNECTED` is the Chromium network error name shown on the interstitial and in `chrome://network-errors/`.
:::

## Examples

The first example covers navigations and images with explicit fallbacks and `Response.error()` for everything else; the second shows the same in Workbox and how the page detects whether a fallback is in place.

### Precaching and serving a page and an image fallback

Precache both assets in `install` so the worker refuses to install without them. The `fetch` handler only intervenes for the two request types that have a fallback; other requests fall through to the network untouched.

```js
const FALLBACK_CACHE = 'fallback-v1';
const OFFLINE_PAGE = '/offline.html';
const OFFLINE_IMAGE = '/offline.svg';

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(FALLBACK_CACHE).then((cache) => cache.addAll([OFFLINE_PAGE, OFFLINE_IMAGE]))
  );
});

self.addEventListener('fetch', (event) => {
  const { request } = event;
  if (request.mode !== 'navigate' && request.destination !== 'image') return;

  event.respondWith((async () => {
    try {
      return await fetch(request);
    } catch {
      const cached = await caches.match(request);
      if (cached) return cached;
      const fallback = await caches.match(request.mode === 'navigate' ? OFFLINE_PAGE : OFFLINE_IMAGE);
      return fallback ?? Response.error();
    }
  })());
});
```

Returning the SVG for a failed `<img>` costs nothing visible beyond the placeholder; returning `/offline.html` for a navigation replaces the entire page, so the handler checks `caches.match(request)` first and only falls back when the exact URL is absent. `Response.error()` at the end preserves the browser's native behaviour for a request type the worker did not plan for.

### Workbox setCatchHandler and page-side detection

`setCatchHandler` runs when every registered route's handler throws, which is what a failed `NetworkFirst` does. `matchPrecache()` reads from the precache created by `precacheAndRoute()`, so `/offline.html` needs to be in the build manifest. On the page, a `HEAD` request to the fallback URL (served from cache when the worker is active) tells whether the fallback is installed; without a worker the page shows a plain "connection lost" message driven by the `offline` event instead.

```js
// sw.js
import { precacheAndRoute, matchPrecache } from 'workbox-precaching';
import { setCatchHandler } from 'workbox-routing';

precacheAndRoute(self.__WB_MANIFEST); // the manifest must include /offline.html

setCatchHandler(async ({ request }) => {
  if (request.destination === 'document') return matchPrecache('/offline.html');
  if (request.destination === 'image') return matchPrecache('/offline.svg');
  return Response.error();
});

// page.js
if (navigator.serviceWorker?.controller) {
  // A controlled page: navigations will get /offline.html on failure.
} else {
  window.addEventListener('offline', () => showBanner('Connection lost. Changes are saved when you are back online.'));
}
```

Workbox adds the response-cloning, status filtering, and cache naming that the hand-written version omits, at the cost of a build step to produce `__WB_MANIFEST`; the `destination === 'document'` test is equivalent to the `mode === 'navigate'` guard above for top-level navigations and also matches iframes.

## See also

- [Manage fallback responses](https://developer.chrome.com/docs/workbox/managing-fallback-responses/) (developer.chrome.com)
- [Response: error() static method](https://developer.mozilla.org/en-US/docs/Web/API/Response/error_static) (developer.mozilla.org)
- [Caching strategies for service workers](/reference/service-worker/caching-strategies/)
- [Cache API](/reference/service-worker/cache-api/)
- [FetchEvent and request routing](/reference/service-worker/fetch-event/)
- [Workbox](/reference/service-worker/workbox/)