How to diagnose and fix service worker registration, update lifecycle, and caching problems. Covers scope issues, update flow, cache versioning, and...
What This Means
Service workers are JavaScript files that run in the background, enabling:
- Offline functionality
- Background sync
- Push notifications
- Asset caching
Common issues include failed registration, stale cache, update problems, and scope conflicts.
How to Diagnose
- DevTools > Application tab
- Click Service Workers in sidebar
- Check:
- Registration status
- Active/waiting workers
- Error messages
2. Console Errors
// Check registration status
navigator.serviceWorker.getRegistrations().then(registrations => {
console.log('Registered SWs:', registrations);
});
// Listen for errors
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('Registered:', reg))
.catch(err => console.error('Failed:', err));
3. Common Error Messages
| Error |
Cause |
| "SecurityError" |
Not HTTPS or localhost |
| "Failed to register" |
SW file not found or syntax error |
| "Scope too wide" |
SW path issue |
| "Update found but waiting" |
skipWaiting not called |
General Fixes
Basic Service Worker
// sw.js - Basic cache-first strategy
const CACHE_NAME = 'app-cache-v1';
const ASSETS = [
'/',
'/styles/main.css',
'/scripts/app.js',
'/offline.html'
];
// Install: cache assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(ASSETS))
.then(() => self.skipWaiting())
);
});
// Activate: clean old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
);
}).then(() => self.clients.claim())
);
});
// Fetch: cache-first strategy
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(cached => cached || fetch(event.request))
.catch(() => caches.match('/offline.html'))
);
});
Registration with Update Handling
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
// Check for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New version available
showUpdateNotification();
}
});
});
console.log('SW registered:', registration.scope);
} catch (error) {
console.error('SW registration failed:', error);
}
});
}
function showUpdateNotification() {
if (confirm('New version available! Reload to update?')) {
window.location.reload();
}
}
Force Update on Change
// sw.js
const VERSION = '2.0.0';
self.addEventListener('install', (event) => {
console.log('Installing SW version:', VERSION);
self.skipWaiting(); // Don't wait for old SW to close
});
self.addEventListener('activate', (event) => {
event.waitUntil(
Promise.all([
// Take control immediately
self.clients.claim(),
// Clear old caches
caches.keys().then(keys =>
Promise.all(keys.map(key => caches.delete(key)))
)
])
);
});
Network-First for API Calls
// sw.js - Different strategies for different requests
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// API calls: network-first
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request));
return;
}
// Static assets: cache-first
if (url.pathname.match(/\.(js|css|png|jpg|svg)$/)) {
event.respondWith(cacheFirst(event.request));
return;
}
// HTML: network-first with offline fallback
event.respondWith(networkFirstWithOffline(event.request));
});
async function networkFirst(request) {
try {
return await fetch(request);
} catch {
return caches.match(request);
}
}
async function cacheFirst(request) {
const cached = await caches.match(request);
return cached || fetch(request);
}
async function networkFirstWithOffline(request) {
try {
return await fetch(request);
} catch {
const cached = await caches.match(request);
return cached || caches.match('/offline.html');
}
}
Fix Stale Cache
// Force clear all caches
async function clearAllCaches() {
const keys = await caches.keys();
await Promise.all(keys.map(key => caches.delete(key)));
console.log('All caches cleared');
}
// Unregister all service workers
async function unregisterAllSWs() {
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map(reg => reg.unregister()));
console.log('All service workers unregistered');
}
// Nuclear option: clear everything
async function hardReset() {
await clearAllCaches();
await unregisterAllSWs();
window.location.reload(true);
}
Using Workbox (Recommended)
// sw.js using Workbox
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// Precache static assets
precacheAndRoute(self.__WB_MANIFEST);
// Cache images
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
}),
],
})
);
// Network-first for API
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
networkTimeoutSeconds: 5,
})
);
Scope Issues
// Service worker scope is determined by its location
// /sw.js -> scope is /
// /app/sw.js -> scope is /app/
// To expand scope (requires header):
// Service-Worker-Allowed: /
navigator.serviceWorker.register('/path/sw.js', {
scope: '/'
});
Verification
- DevTools > Application > Service Workers shows "Activated"
- No console errors on registration
- Offline page works when network disabled
- Updates apply after refresh
Common Mistakes
| Mistake |
Fix |
| Not using HTTPS |
Required (except localhost) |
| SW file 404 |
Check file path and server config |
| Caching too aggressively |
Use versioned cache names |
| Not calling skipWaiting |
Call in install event |
| Forgetting clients.claim |
Call in activate event |
| Caching API responses long-term |
Use network-first for dynamic data |
Further Reading