How to Fix 'TypeError: Failed to fetch' in Service Workers
Why service workers throw 'TypeError: Failed to fetch', how it differs from regular fetch errors, and fixes for caching, navigation, and offline scenarios.
How to Fix “TypeError: Failed to fetch” in Service Workers
The error in your console:
Uncaught (in promise) TypeError: Failed to fetch
When this error comes from a service worker, it behaves differently than a regular fetch failure. In normal page JavaScript, Failed to fetch usually means a network problem or a CORS issue. Inside a service worker, it can mean several additional things: an opaque response being stored incorrectly, a respondWith call receiving a bad response, or a cache miss with no fallback.
The tricky part is that the error message is identical in all cases. You have to look at the context to figure out the cause.
How service worker fetch works
When a service worker is active, every network request from the page goes through the service worker’s fetch event handler first. The handler decides what to do: pass the request to the network, return a cached response, or construct a response from scratch.
self.addEventListener('fetch', (event) => {
event.respondWith(
// Your logic here decides what response to return
)
})
If anything goes wrong inside respondWith, the page sees TypeError: Failed to fetch. The service worker swallowed the real error and replaced it with a generic failure.
Common causes
1. Caching an opaque response with cache.put
When a service worker fetches a cross-origin resource without CORS, the response is “opaque” (its type property is "opaque"). Opaque responses have a status of 0 and no readable body. You can store them in the cache, but only with cache.put used carefully.
The problem: if you clone the response incorrectly or try to read the body before caching, the response stream is consumed and the cache operation fails.
// This can fail silently or throw
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('v1').then((cache) => {
return fetch(event.request).then((response) => {
cache.put(event.request, response) // response body is consumed
return response // empty body, page gets nothing
})
})
)
})
Fix: Always clone the response before caching it.
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('v1').then((cache) => {
return fetch(event.request).then((response) => {
cache.put(event.request, response.clone())
return response
})
})
)
})
For opaque responses specifically, use cache.add or cache.addAll instead of manually fetching and putting. These methods handle opaque responses correctly:
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/',
'/styles.css',
'https://cdn.example.com/library.js', // opaque response handled correctly
])
})
)
})
2. respondWith receives a rejected promise
If the promise passed to event.respondWith() rejects, the browser surfaces it as TypeError: Failed to fetch on the page. The actual rejection reason is logged only in the service worker’s console scope.
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached
return fetch(event.request) // rejects if offline with no fallback
})
)
})
Fix: Always provide a fallback in the catch handler.
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((cached) => {
if (cached) return cached
return fetch(event.request)
})
.catch(() => {
// Return a fallback for navigation requests
if (event.request.mode === 'navigate') {
return caches.match('/offline.html')
}
return new Response('Network error', {
status: 503,
headers: { 'Content-Type': 'text/plain' },
})
})
)
})
3. Calling respondWith too late
event.respondWith() must be called synchronously within the fetch event handler. If you call it inside a setTimeout, or after an await that isn’t the first thing in the handler, the browser has already started handling the request normally and your respondWith call fails.
// Broken: respondWith called after an unrelated await
self.addEventListener('fetch', async (event) => {
const config = await getConfig() // too late after this
event.respondWith(
fetch(event.request)
)
})
Fix: Call respondWith synchronously and do async work inside the promise you pass to it.
self.addEventListener('fetch', (event) => {
event.respondWith(
(async () => {
const config = await getConfig()
const cached = await caches.match(event.request)
if (cached) return cached
return fetch(event.request)
})()
)
})
4. Navigation requests failing in cache-first strategies
Navigation requests (clicking a link, typing a URL) have mode: 'navigate'. If your service worker uses a cache-first strategy and the navigation page is not in the cache, the fetch to the network fails (offline or server down), and there’s no fallback.
Fix: Use network-first for navigation requests, with a cached offline page as fallback.
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.catch(() => caches.match('/offline.html'))
)
return
}
// Cache-first for static assets
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request)
})
)
})
5. Service worker scope mismatch
The service worker only intercepts requests within its scope. If you register it at /app/sw.js, it only handles requests under /app/. Requests outside that scope bypass the service worker entirely and fail normally if the network is down.
Fix: Register the service worker at the root, or use the Service-Worker-Allowed header to expand its scope.
// Register at root scope
navigator.serviceWorker.register('/sw.js', { scope: '/' })
Debugging in Chrome DevTools
Open DevTools and go to the Application tab. Under Service Workers, you can see:
- The registered service worker and its status
- A “Update on reload” checkbox (enable this during development)
- Console output from the service worker (separate from the page console)
Go to Application > Cache Storage to inspect what’s actually cached. Missing entries here explain why cache-first strategies fail.
The Network tab shows requests routed through the service worker with a gear icon. Filter by “Service Worker” to see only intercepted requests.
Prevention
Use a tested service worker library like Workbox instead of writing fetch handlers from scratch. Workbox provides pre-built strategies (cache-first, network-first, stale-while-revalidate) that handle edge cases like opaque responses and navigation fallbacks.
import { precacheAndRoute } from 'workbox-precaching'
import { registerRoute, NavigationRoute } from 'workbox-routing'
import { NetworkFirst } from 'workbox-strategies'
precacheAndRoute(self.__WB_MANIFEST)
registerRoute(
new NavigationRoute(
new NetworkFirst({ cacheName: 'pages' })
)
)
Hushbug catches TypeError and failed fetch errors from service workers automatically while you browse. Coming soon to the Chrome Web Store.