Skip to content

Offline fallback for service workers

In one line: An offline fallback is a pre-cached HTML page (or API response) that your service worker returns when a navigation request fails and no cached version of the requested URL exists — so users see something useful instead of a browser error screen.

When a Network First strategy fails (no network, nothing cached for that URL), the service worker can either:

  1. Let the request fall through — the browser shows its own offline error page (a dead end for the user).
  2. Return a pre-cached fallback — a branded, helpful response that tells the user they are offline.

A fallback does not replace per-URL caching; it is the safety net for URLs that have never been loaded or whose cache entry has expired.

Pre-cache the offline page during the install event so it is always available:

const FALLBACK_URL = '/offline.html';
const CACHE_NAME = 'offline-fallback-v1';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.add(FALLBACK_URL))
);
});

For single-page apps, the fallback is usually /index.html (the app shell), already pre-cached. No separate offline page is needed if the shell itself renders a useful “you are offline” state.

Return the fallback only for navigation requests that fail both network and cache:

self.addEventListener('fetch', (event) => {
if (event.request.mode !== 'navigate') return;
event.respondWith(
fetch(event.request).catch(() =>
caches.match(event.request).then(
(cached) =>
cached ?? caches.open(CACHE_NAME).then((c) => c.match(FALLBACK_URL))
)
)
);
});

This pattern:

  1. Tries the network.
  2. On failure, checks whether the exact URL is cached.
  3. If still nothing, returns the generic offline fallback.

For non-navigation requests you can serve a fallback SVG or placeholder instead of a network error:

const IMAGE_FALLBACK = '/fallback-image.svg';
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image') {
event.respondWith(
fetch(event.request).catch(() => caches.match(IMAGE_FALLBACK))
);
}
});

For JSON API requests, return a structured fallback rather than an HTML error page:

self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(event.request).catch(() =>
new Response(JSON.stringify({ error: 'offline' }), {
headers: { 'Content-Type': 'application/json' },
status: 503,
})
)
);
}
});

Workbox’s setCatchHandler is the idiomatic way to handle unmatched fetch failures:

import { setCatchHandler, NavigationRoute, registerRoute } from 'workbox-routing';
import { precacheAndRoute, matchPrecache } from 'workbox-precaching';
// Pre-cache the fallback as part of your manifest
precacheAndRoute(self.__WB_MANIFEST); // includes /offline.html
// Return the offline page for any failed navigation
setCatchHandler(async ({ event }) => {
if (event.request.destination === 'document') {
return matchPrecache('/offline.html');
}
return Response.error();
});

A good offline fallback page:

  • Clearly tells the user they are offline — do not leave them guessing why content failed to load.
  • Remains functional without network — avoid scripts that require API calls. The page must render purely from its pre-cached HTML.
  • Reflects your product’s visual style — not a generic browser error.
  • Links to cached content where practical (e.g., “Pages you’ve visited recently”).
  • Pre-cache the fallback in install with event.waitUntil(cache.add('/offline.html')).
  • Only serve the generic fallback for navigation requests (mode === 'navigate'); subresource failures should fail silently or return typed fallbacks.
  • Test the fallback by opening DevTools → Network → “Offline” and navigating to an uncached URL.
  • For SPAs, verify the app shell itself handles the offline state gracefully without a separate offline page.
  • Version the fallback cache name and clean it up in activate alongside other old caches.
  • Caching strategies — Network First is the strategy most often paired with a fallback
  • The Cache APIcache.add() for pre-caching; caches.match() at request time
  • WorkboxsetCatchHandler and matchPrecache