The Problem

Service workers cache assets aggressively to enable offline functionality. After a deployment, users may continue to see the old cached version of your app, causing crashes when old JavaScript tries to communicate with new APIs.

Symptoms

  • Users report app crashes after a deployment
  • "Unexpected token" or "Cannot read property" errors
  • Refreshing the page does not fix the issue
  • Clearing browser cache resolves the problem
  • Old users have bugs but new users do not

Real Error Message

bash
TypeError: Cannot read properties of undefined (reading 'fetchAll')
    at main-abc123.js:1
    The error occurs because the cached main.js references a function
    that no longer exists in the new backend API.

Root Cause

The service worker has cached main.js, styles.css, and index.html. After deployment, new files are generated (e.g., main-def456.js) but the service worker continues serving the old cached versions.

How to Fix It

Fix 1: Cache Versioning

```javascript // sw.js const CACHE_NAME = 'my-app-v3'; // Increment on each deployment const ASSETS = ['/index.html', '/main.js', '/styles.css'];

self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS)) ); });

self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(names => Promise.all(names.filter(n => n !== CACHE_NAME).map(n => caches.delete(n))) ) ); }); ```

Fix 2: Network-First Strategy for HTML

javascript
self.addEventListener('fetch', event => {
  if (event.request.destination === 'document') {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          const clone = response.clone();
          caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
          return response;
        })
        .catch(() => caches.match('/index.html'))
    );
  }
});

Fix 3: Force Update on New Service Worker

javascript
// In your app's main.js
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(registration => {
    registration.addEventListener('updatefound', () => {
      const newWorker = registration.installing;
      newWorker.addEventListener('statechange', () => {
        if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
          // New SW is ready, prompt user to reload
          if (confirm('New version available. Reload?')) {
            newWorker.postMessage({ action: 'skipWaiting' });
            window.location.reload();
          }
        }
      });
    });
  });
}
javascript
// In sw.js
self.addEventListener('message', event => {
  if (event.data.action === 'skipWaiting') {
    self.skipWaiting();
  }
});

Fix 4: Hash-Based Cache Keys

javascript
// Build tool generates files with content hashes
// main.abc123.js instead of main.js
// When content changes, the filename changes, bypassing cache

Fix 5: Workbox Cache Expiration

```javascript import { ExpirationPlugin } from 'workbox-expiration'; import { StaleWhileRevalidate } from 'workbox-strategies';

registerRoute( /\.js$/, new StaleWhileRevalidate({ cacheName: 'js-cache', plugins: [ new ExpirationPlugin({ maxEntries: 30, maxAgeSeconds: 7 * 24 * 60 * 60 // 7 days }) ] }) ); ```