# FetchEvent and request routing

> The FetchEvent members for every in-scope request, the respondWith() and waitUntil() rules and their error outcomes, and routing by destination, mode, and URL.

A `FetchEvent` is dispatched in the service worker for every navigation and subresource request made by a controlled client; calling `event.respondWith()` replaces the network response with any `Response`, and not calling it lets the request proceed to the network unchanged. The interface is available in Chrome 40, Edge 17, Firefox 44, and Safari 11.1 (BCD `api.FetchEvent`), with `preloadResponse` and `handled` added later and covered on their own entries.

## Syntax

```js
self.addEventListener('fetch', (event) => {
  event.respondWith(responsePromise);   // optional: take over the response
  event.waitUntil(promise);             // optional: keep the worker alive for side effects
});
```

`respondWith()` must be called synchronously inside the handler; after the handler returns, the browser has already decided whether to go to the network. `waitUntil()` can be called at any point while the event is still being handled, including from inside the `respondWith()` promise chain.

## Members

The event exposes the request and the client identifiers; the two methods control the response and the worker's lifetime.

| Member | Type | Description |
|---|---|---|
| `request` | `Request` | The intercepted request. `url`, `method`, `headers`, `mode`, `destination`, and `credentials` are the properties used for routing. |
| `respondWith(r)` | `void` | Takes `Response` or `Promise<Response>`. The promise's settlement is the response the client receives. |
| `waitUntil(p)` | `void` | Extends the event's lifetime until `p` settles, so a cache write started after `respondWith()` is not cut off when the worker is terminated. |
| `clientId` | `string` | Id of the client that made the request. Empty for a navigation, because the navigating client does not exist yet. |
| `resultingClientId` | `string` | Id of the client a navigation will create. Empty for subresource requests. |
| `replacesClientId` | `string` | Id of the client being replaced by a navigation (same-window navigations only). |
| `handled` | `Promise<void>` | Resolves when the response has been handed to the client, or rejects if the event ended with a network error (BCD `api.FetchEvent.handled`). |
| `preloadResponse` | `Promise<Response \| undefined>` | The navigation-preload response, or `undefined` when preload did not run; see the `NavigationPreloadManager` entry. |

`request.destination` is the most reliable routing key: `'document'` for navigations, `'script'`, `'style'`, `'image'`, `'font'`, and `''` for `fetch()` and `XMLHttpRequest` calls. `request.mode === 'navigate'` is equivalent to `destination === 'document'` for top-level pages but also matches iframes, whose destination is `'iframe'`.

## Exceptions

`respondWith()` throws synchronously; the response-side failures are delivered to the client as a network error, not as an exception in the worker.

- `InvalidStateError`: `respondWith()` was called after the handler returned, or called a second time on the same event.
- Network error to the client: the promise passed to `respondWith()` rejects, or resolves to something that is not a `Response`. Chromium logs `The FetchEvent for "<url>" resulted in a network error response: an object that was not a Response was passed to respondWith().` in the worker console and the page sees `TypeError: Failed to fetch` (or an error page for navigations).
- Network error to the client: the `Response` body has already been used, for example because it was passed to `cache.put()` without `clone()`.
- `TypeError` from `new Request(event.request, init)` when the request is a navigation (`mode: 'navigate'`) and `init` is not empty; build a new `Request` from `event.request.url` instead when a navigation request needs modified headers.

:::observed
In Chrome (English UI), a handler that does `event.respondWith(fetch(event.request).then(() => undefined))` makes the page request fail with `net::ERR_FAILED` in the Network panel, and the service worker console shows `The FetchEvent for "https://example.com/app.js" resulted in a network error response: an object that was not a Response was passed to respondWith().`; both halves of that sentence are string constants in Chromium's `fetch_respond_with_observer.cc`.
:::

## Examples

The handlers below are written so that only the branch that matches calls `respondWith()`; every other request falls through to the network.

### Routing by destination and URL prefix

Images get a cache-first helper, API calls bypass the cache, and everything else is left to the browser.

```js
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  if (url.origin !== self.location.origin) return;          // third-party: do not intercept

  if (event.request.destination === 'image') {
    event.respondWith(cacheFirst(event.request, 'images-v1'));
  } else if (url.pathname.startsWith('/api/')) {
    event.respondWith(fetch(event.request));                 // explicit network-only
  }
});

async function cacheFirst(request, cacheName) {
  const cache = await caches.open(cacheName);
  const cached = await cache.match(request);
  if (cached) return cached;
  const response = await fetch(request);
  if (response.ok) cache.put(request, response.clone());
  return response;
}
```

Returning early for other origins matters even for a network-only route: once `respondWith()` is called, the worker is responsible for the full response, including CORS behaviour and opaque responses.

### Serving the app shell for navigations with `waitUntil()`

A single-page app answers every navigation from the cached shell and refreshes that shell in the background, keeping the worker alive for the refresh.

```js
self.addEventListener('fetch', (event) => {
  if (event.request.mode !== 'navigate') return;
  event.respondWith(
    caches.match('/index.html').then((cached) => cached ?? fetch(event.request))
  );
  event.waitUntil(
    fetch('/index.html').then((fresh) =>
      fresh.ok ? caches.open('shell-v1').then((c) => c.put('/index.html', fresh)) : undefined
    ).catch(() => {})
  );
});
```

Without `waitUntil()` the browser may terminate the worker as soon as `respondWith()` settles, dropping the background refresh. The `catch` prevents an offline refresh from surfacing as an unhandled rejection.

### Detecting service worker support and keeping fetches working without it

Pages served without a controlling worker (first visit, insecure origins, a hard reload) must still load. Register conditionally and do not make page code depend on interception.

```js
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').catch((err) => {
    console.warn('Service worker registration failed; running without offline support', err);
  });
}
// Page code calls fetch('/api/items') the same way in both cases: the worker
// is an optimisation layer, so the request works when no worker is in control.
```

`navigator.serviceWorker.controller` is `null` until a worker controls the page, which is also the state after a hard reload (Shift+Reload) in Chrome and Firefox; code that reads it must handle `null`.

## See also

- [Service Workers specification: FetchEvent interface](https://w3c.github.io/ServiceWorker/#fetchevent-interface) (w3c.github.io)
- [FetchEvent: respondWith() method](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith) (developer.mozilla.org)
- [FetchEvent](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent) (developer.mozilla.org)
- [Caching strategies for service workers](/reference/service-worker/caching-strategies/)
- [Cache API](/reference/service-worker/cache-api/)
- [NavigationPreloadManager](/reference/service-worker/navigation-preload/)
- [Offline fallback](/reference/service-worker/offline-fallback/)