This page gives the manual counterpart to the automated pipeline in Service Worker Error Telemetry & Debugging: what to do once a report tells you something is wrong.
The exact problem #
A user reports that the app shows an old version and an outdated offline page. You reload — everything is fine. You clear your cache — still fine. The bug lives in a state you do not have: an old worker still controlling their tab, a precache from a previous build, and an update that has been waiting since Tuesday.
Debugging service workers is mostly about reading state accurately before changing it. The most common mistake is clicking “Clear site data” first, which destroys the evidence and produces a working app that proves nothing.
The panel, in the order you should read it #
Open DevTools → Application → Service Workers and read top to bottom before touching any control.
| What you see | What it means | What to do next |
|---|---|---|
#4321 activated and is running |
Current worker is in control and awake | Compare its script URL hash with your latest deploy |
#4322 waiting to activate |
A new worker installed but cannot take over | At least one tab is still controlled — this is the update-prompt case |
installing that never completes |
The install handler is throwing or a precache request 404s | Check the Console for the install error; verify the manifest |
activated and is stopped |
Normal — the browser idles workers aggressively | Nothing; it wakes on the next event |
| No worker listed | Registration failed or was unregistered | Check the registration call and its scope |
redundant |
Replaced or failed | The Console retains the reason from the failed install |
Diffing Cache Storage against the manifest #
Most “the offline page is wrong” reports are a cache/manifest mismatch. Run this in the page console, not the worker’s, so you can paste your manifest in easily.
// Paste in the page console: compares what is cached with what should be.
async function auditPrecache(expected /* array of URLs from your build manifest */) {
const names = await caches.keys();
const report = {};
for (const name of names) {
const cache = await caches.open(name);
const keys = await cache.keys();
const cached = new Set(keys.map((r) => new URL(r.url).pathname + new URL(r.url).search));
report[name] = {
size: cached.size,
missing: expected.filter((u) => !cached.has(u)),
orphaned: [...cached].filter((u) => !expected.includes(u)),
};
}
console.table(
Object.entries(report).map(([name, r]) => ({
cache: name, entries: r.size, missing: r.missing.length, orphaned: r.orphaned.length,
})),
);
return report;
}
Read the result this way: missing entries mean the install handler failed partway or the manifest changed after the precache; orphaned entries mean the activate cleanup did not run or filtered on a cache name pattern that stopped matching. Both point at the lifecycle handlers described in App Shell Precache & Offline Fallback Routing rather than at the fetch handler most people look at first.
A reproducible debugging loop #
- Reproduce the user’s state, do not fix it. If they are stuck on an old worker, use “Update on reload” off and unregister nothing until you have read the state.
- Check the script URL. The listed source URL and its query/hash tell you which build is in control. Half of all reports end here.
- Look for install errors in the Console. A single 404 in a precache list aborts the whole install, leaving a permanently
installingworker and a user stuck on the previous build. - Audit the caches. The script above, against the deployed manifest.
- Simulate offline in the Service Workers pane. This exercises the worker’s own fetch path; the Network panel’s offline toggle does not always cover the same branch.
- Change one thing.
skipWaitingpromotes the waiting worker; “Update” re-fetches the script; “Unregister” removes it entirely. Each answers a different question, and doing all three at once answers none.
// A safer alternative to "Clear site data" — clear caches only, keep IndexedDB.
async function clearCachesOnly() {
const names = await caches.keys();
await Promise.all(names.map((n) => caches.delete(n)));
console.info(`cleared ${names.length} cache(s); IndexedDB untouched`);
}
That distinction matters when the bug is in recovery: IndexedDB is where the drafts and snapshots live, and clearing it deletes the very records you are debugging — the data described in Form State Recovery & Multi-Step Wizards.
Verification #
- Prove you can reproduce the stuck state. With “Update on reload” off, deploy a change and confirm you see
waiting to activate— that is the state your users are in. - Break a precache entry. Add a nonexistent URL to the manifest; the worker must stay
installingand the Console must name the failing request. This teaches you the signature. - Run the cache audit. Confirm zero missing and zero orphaned entries on a healthy build; anything else is a real finding.
- Test offline for real. Check the offline box, navigate to an uncached route, and confirm the offline fallback renders — the behaviour covered in Serving an Offline Fallback Page from the Service Worker.
- Confirm your clean-up is surgical. After
clearCachesOnly(), an IndexedDB draft written before the clear must still be there.
One habit worth adopting: before changing anything, copy the worker’s script URL, the cache key list and the registration state into the incident notes. Those three values are what let you compare a user’s broken state with a healthy one later, and every DevTools action you take from that point destroys some of them.
Frequently asked questions #
Why does my new service worker stay stuck in ‘waiting’?
Because a client is still controlled by the old worker; waiting is by design. Click skipWaiting while debugging and ship a real update prompt for users.
Why does the offline checkbox behave differently from network throttling?
The Service Workers pane’s checkbox exercises the worker’s own request path; the Network panel’s offline mode affects page requests, and they do not always cover the same branch.
Is ‘Clear site data’ safe while debugging a recovery bug?
It deletes exactly what you are inspecting. Copy the relevant records first, then clear specific buckets.
Related #
- Service Worker Error Telemetry & Debugging — the automated side of the same investigation
- Logging Service Worker Errors to Your Backend Reliably — getting the reports that start this workflow
- Recovering from a Stuck Service Worker with skipWaiting and clients.claim — fixing the waiting state in production, not just in DevTools