Concept in Service Worker
Debugging service workers
Published
Debugging a service worker means reading three things the page cannot show on its own: the registration’s state (installing, waiting, active), the contents of Cache Storage, and which requests the worker answered. Chrome 40+ and Edge 79+ expose all three in DevTools › Application; Firefox 44+ splits them between about:debugging and the Storage panel; Safari 11.1+ exposes registrations in Web Inspector but offers no cache viewer or forced-update control (BCD api.ServiceWorker; vendor docs in the sources).
How it works
Section titled “How it works”The panels differ, but each one is a view onto the same registration object and the same caches the worker sees, so anything a panel does can also be done from the worker’s console context.
Chrome and Edge DevTools
Section titled “Chrome and Edge DevTools”Application › Service workers lists every registration for the origin with its scope, script URL, and a status line of the form #<id> activated and is running, #<id> waiting to activate, or #<id> trying to install. Three checkboxes at the top change behaviour for the current tab only: Offline makes fetch() inside the worker and from the page fail as if the network were down, Update on reload forces a byte-comparison check and installs the fetched script as a new worker on every navigation (bypassing the 24-hour script cache rule), and Bypass for network passes all requests straight to the network without firing fetch in the worker. Each registration has Update, Unregister, and, for a worker in the waiting state, a skipWaiting link that calls skipWaiting() on it without a code change. Application › Cache storage lists caches as <cache name> - <origin> and shows each entry’s request URL, response headers, and a body preview, with per-entry and per-cache delete. In the Network panel, the Size column reads (ServiceWorker) for responses the worker produced, and requests the worker itself issued carry a gear icon. The console’s context selector (top-left of the Console panel, default top) lists the worker by its script URL; selecting it routes console.log from the worker and lets you evaluate await caches.keys() or await self.clients.matchAll() in the worker’s global scope.
Firefox DevTools
Section titled “Firefox DevTools”about:debugging#/runtime/this-firefox lists registrations under Service Workers with scope, state, and two buttons: Inspect, which opens a toolbox whose Console is the worker’s context, and Unregister. The toolbox for a page also shows the worker under Application › Service Workers (Firefox 79+), with a Start button for a stopped worker. Firefox has no “update on reload” switch; the equivalent is registration.update() from the page console, or Unregister followed by a reload. Cache Storage appears in the Storage panel under Cache Storage, listing caches and their request URLs and letting you delete entries.
Safari Web Inspector
Section titled “Safari Web Inspector”Enable the Develop menu (Safari › Settings › Advanced › Show features for web developers, labelled Show Develop menu in menu bar before Safari 17) and open Develop › Service Workers, which lists registrations by scope and opens a Web Inspector window attached to the worker’s context. The Storage tab shows IndexedDB and Local Storage but not Cache Storage, so await caches.keys() and await (await caches.open(name)).keys() in the worker console are the way to list cached entries. There is no offline simulation or forced update; use Network Link Conditioner (macOS) or airplane mode (iOS) and registration.update().
Reading the symptoms
Section titled “Reading the symptoms”The failure modes below each have a visible signature in one of the panels.
| Symptom | Where it shows | Cause and fix |
|---|---|---|
| New code not running | Chrome: waiting to activate beside a second worker |
Old tabs are still controlled; close them, click skipWaiting, or ship a skipWaiting() path |
| Deploy ignored for up to a day | Network panel shows the script served (disk cache) |
HTTP caching of the script; register with updateViaCache: 'none' or send Cache-Control: no-cache on the script URL |
| Requests not intercepted | Network Size column lacks (ServiceWorker) |
The page is outside the scope, the worker is not yet active, or Bypass for network is on |
| Cache empty after install | Cache storage shows no cache, worker console shows a rejected addAll() |
A precache URL returned a non-2xx status; addAll() is atomic and stores nothing on any failure |
| Install hangs | Status stuck at trying to install |
event.waitUntil() received a promise that does not settle |
Examples
Section titled “Examples”The two snippets reproduce the two most-used panel actions from code, so they work in browsers without the corresponding button.
Forcing an update check and listing cache contents from the page console
Section titled “Forcing an update check and listing cache contents from the page console”registration.update() performs the same byte-comparison check that “Update on reload” triggers; caches.keys() from the page lists the same caches the Application panel shows, because caches is shared between page and worker on one origin.
const registration = await navigator.serviceWorker.getRegistration();await registration.update(); // fetches /sw.js, installs if it differsconsole.log(registration.waiting?.state); // "installed" when an update is waiting
for (const name of await caches.keys()) { const cache = await caches.open(name); const keys = await cache.keys(); console.log(name, keys.map((request) => request.url));}The page-side listing works in Safari too, which has no cache viewer; the cost is that opaque responses show only their URL, since an opaque body is unreadable in every context.
A debug flag that logs every fetch decision, with a no-op in production
Section titled “A debug flag that logs every fetch decision, with a no-op in production”Logging inside the fetch handler is the manual equivalent of Workbox’s development-build logs. Keying it on the registration URL keeps the production worker silent without a build step; when console is unavailable (older WebView builds) the guard falls back to doing nothing.
const DEBUG = new URL(self.location.href).searchParams.has('debug'); // register('/sw.js?debug')
self.addEventListener('fetch', (event) => { event.respondWith((async () => { const cached = await caches.match(event.request); if (DEBUG && typeof console !== 'undefined') { console.log(cached ? 'cache' : 'network', event.request.method, event.request.url); } return cached ?? fetch(event.request); })());});Registering /sw.js?debug installs a different script URL and therefore a separate worker; remember to re-register without the query string before measuring, otherwise the logging worker stays in control of that tab.
See also
Section titled “See also”- Debug Progressive Web Apps (developer.chrome.com)
- about:debugging (firefox-source-docs.mozilla.org)
- skipWaiting() and the update flow
- Service worker lifecycle
- Cache API
- Service worker registration and scope
Specifications
| Specification | Status |
|---|---|
| None. | |