Skip to content

The service worker fetch event and routing

In one line: Every network request made by a page in the service worker’s scope fires a fetch event in the worker. event.respondWith(promise) lets the worker intercept the request and return any Response — from cache, network, or constructed inline.

A FetchEvent carries:

  • event.request — the Request object, including url, method, headers, destination, and mode.
  • event.respondWith(responsePromise) — call this to take control of the response. If you don’t call it, the request falls through to the network as normal.
  • event.waitUntil(promise) — extend the worker’s lifetime past the event, for side effects like cache warming.
  • event.clientId — the identifier of the client (tab) that made the request.
  • event.resultingClientId — the new client ID if a navigation request creates a new client.
self.addEventListener('fetch', (event) => {
// Only intercept requests to our own origin
if (!event.request.url.startsWith(self.location.origin)) return;
event.respondWith(
caches.match(event.request).then((cached) => cached ?? fetch(event.request))
);
});

Returning without calling respondWith() passes the request through to the browser’s normal network stack.

Property Values and meaning
request.destination 'document', 'script', 'image', 'font', 'fetch', '' (XHR/fetch with no type)
request.mode 'navigate' (page navigation), 'cors', 'no-cors', 'same-origin'
request.method 'GET', 'POST', 'PUT', etc.
request.url Full URL string; use new URL(event.request.url) for pathname matching
self.addEventListener('fetch', (event) => {
const { destination } = event.request;
if (destination === 'image') {
event.respondWith(cacheFirst(event.request, 'images'));
} else if (destination === 'document') {
event.respondWith(networkFirst(event.request, 'pages'));
}
// other requests fall through
});
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkOnly(event.request));
} else if (url.pathname.startsWith('/static/')) {
event.respondWith(cacheFirst(event.request, 'static'));
}
});
Section titled “Navigation requests (for SPA offline support)”

For single-page apps, all navigate requests should return the cached app shell:

self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(
caches.match('/index.html').then((cached) => cached ?? fetch(event.request))
);
return;
}
// handle other request types…
});

respondWith() accepts any Response, including one you construct:

event.respondWith(
new Response('<h1>Offline</h1>', {
headers: { 'Content-Type': 'text/html' },
})
);

This is the basis for offline fallback pages. See Offline fallback.

  • Non-interceptable requests. Requests with mode: 'no-cors' from <link rel="preload"> or third-party iframes may not fire a fetch event depending on context.
  • Opaque responses. Cross-origin requests fetched with no-cors produce opaque responses (status: 0). You can cache and return them, but cannot read their body or status.
  • POST and mutation requests. The fetch event fires for POST too, but caching mutation responses requires careful design (see Background Sync for deferring mutations offline).

Workbox’s registerRoute is a cleaner way to express routing rules than a long if/else chain in the fetch listener:

import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst } from 'workbox-strategies';
registerRoute(({ request }) => request.destination === 'image', new CacheFirst());
registerRoute(({ request }) => request.mode === 'navigate', new NetworkFirst());

See Workbox for full details.

  • Only call event.respondWith() when you intend to handle the request; let everything else fall through.
  • Check event.request.method before caching — never cache non-GET responses unless you specifically need to.
  • Use new URL(event.request.url).origin to distinguish same-origin from cross-origin requests before routing.
  • Handle the case where both cache and network fail — return a fallback Response to avoid a blank error page.
  • Test routing logic with DevTools Network throttling set to “Offline”.
  • Caching strategies — the strategies invoked by respondWith()
  • The Cache APIcaches.match() and related methods used inside handlers
  • Offline fallback — constructing a meaningful response when everything fails
  • Workbox — production-ready routing and strategy library