Skip to content

The Fetch Event

Once a service worker is activated and controls a page, every network request the page makes — HTML, CSS, JS, images, API calls — passes through the SW’s fetch event. This is the heart of offline-first PWAs.

Inside the fetch listener, call event.respondWith() with a Promise that resolves to a Response. If you do not call event.respondWith(), the request falls through to the network normally (passthrough behaviour).

self.addEventListener('fetch', (event) => {
// Simplest possible passthrough — behaves as if no SW is installed
event.respondWith(fetch(event.request));
});

Try the network; if it fails (offline or server error), fall back to the cache.

self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// Clone the response: a Response body can only be consumed once
const clone = response.clone();
caches.open('v1').then((cache) => cache.put(event.request, clone));
return response;
})
.catch(() => caches.match(event.request))
);
});

Serve from cache immediately; if not cached, go to the network and cache the result.

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached;
return fetch(event.request).then((response) => {
const clone = response.clone();
caches.open('v1').then((cache) => cache.put(event.request, clone));
return response;
});
})
);
});

Return the cached version instantly, then update the cache in the background.

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('v1').then((cache) =>
cache.match(event.request).then((cached) => {
const networkFetch = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
return cached || networkFetch;
})
)
);
});
sequenceDiagram
  participant P as Page
  participant SW as Service Worker
  participant C as Cache Storage
  participant N as Network

  P->>SW: fetch(request)
  SW->>C: caches.match(request)
  alt cache hit
    C-->>SW: cached Response
    SW-->>P: Response (instant)
  else cache miss
    SW->>N: fetch(request)
    N-->>SW: fresh Response
    SW->>C: cache.put(request, response.clone())
    SW-->>P: Response
  end
Cache-first fetch flow through the service worker

Always clone before caching. A Response body is a readable stream that can only be consumed once. If you pass the same Response to both the caller and cache.put(), one of them will receive an empty body. Use response.clone() before storing.

Do not intercept cross-origin opaque responses carelessly. Requests to third-party origins without CORS return an opaque response (type === 'opaque') with status === 0. Caching opaque responses inflates cache storage unpredictably. Apply fetch-event logic only to same-origin requests unless you know what you are doing.

self.addEventListener('fetch', (event) => {
// Only handle same-origin requests
if (!event.request.url.startsWith(self.location.origin)) return;
event.respondWith(
caches.match(event.request).then((cached) => cached || fetch(event.request))
);
});
What happens if the fetch handler calls event.respondWith() with a Promise that rejects?
Why must you clone a Response before passing it to cache.put()?
Which fetch strategy returns a cached response immediately and updates the cache in the background?
What is an opaque response?