Fixing "Service worker registration failed"
Diagnose 'Service worker registration failed. Status code: 3/15' in an MV3 extension: wrong paths, top-level exceptions, module type mismatches, unsupported syntax, importScripts errors and missing files in the build.
Table of Contents
You load the extension and chrome://extensions shows an error: “Service worker registration failed. Status code: 15” — or status 3 — and the “Inspect views: service worker” link is missing or labelled “(Inactive)”. Nothing in the extension works, because the background never started. The message is terse, but the status code narrows the cause considerably, and almost every case comes down to one of a handful of problems: the file is not where the manifest says, the script throws during its first run, or the manifest and the code disagree about whether the worker is an ES module. This guide walks through the diagnosis. It belongs to service worker fundamentals.
What the status codes mean
Chrome registers the extension’s service worker at install and on every reload, then starts it. Registration fails if Chrome cannot fetch or evaluate the script. The numeric status comes from Chromium’s service worker status codes. 15 means script evaluation failed: the file was found and loaded, but running its top-level code threw — a syntax error, a ReferenceError such as using window or document, an exception in code that runs at import time, or an import statement in a non-module worker. 3 means start worker failed: commonly the file path is wrong, the file is missing from the build, or a module import could not be resolved. Other codes are rarer. In all cases, the cause is usually visible in the “Errors” view on chrome://extensions, which shows the underlying exception and line.
Step-by-step: find and fix the cause
1. Read the real error
1chrome://extensions → Developer mode on → your extension → "Errors"
2 Uncaught ReferenceError: window is not defined
3 Context: Service worker Stack: sw.js:3 (anonymous function)
Execution context: the extensions page. The Errors view records exceptions thrown during registration with file and line — far more useful than the status code. If it is empty, click “Service worker” under “Inspect views” if present, or use chrome://serviceworker-internals to find the worker and its last error. Clear old errors before reloading so you read only the current failure.
2. Check the path matches the build output
1// manifest.json in the BUILT extension folder
2{ "background": { "service_worker": "background/sw.js", "type": "module" } }
1ls dist/chrome/background/sw.js # must exist, relative to the folder you loaded
Execution context: the built extension. The path is relative to the extension root — the folder containing manifest.json — not to your source tree. A bundler that outputs sw.js at the root while the manifest says background/sw.js produces status 3. So does loading the source folder instead of the dist folder by mistake. The path must not start with / or ./ in some versions; plain relative paths are safest.
3. Align module type with the code
1// If sw.js contains `import` statements:
2"background": { "service_worker": "sw.js", "type": "module" }
3
4// If sw.js uses importScripts():
5"background": { "service_worker": "sw.js" } // classic worker, no "type"
Execution context: the manifest. An import statement in a classic worker is a syntax error (status 15); importScripts in a module worker throws (status 15). Module workers also require every imported path to be resolvable relative to the importing file and to include the .js extension — bare specifiers like import x from "lodash" fail unless bundled. See using ES modules in an MV3 service worker.
4. Remove DOM and window references from top-level code
1// Fails at evaluation: libraries or code touching window/document on import
2// import someLib from "./lib-that-reads-window.js";
3
4// Guard shared code
5const g = globalThis;
6const isWorker = typeof g.document === "undefined";
Execution context: the service worker and every module it imports. A dependency that reads window.navigator or document.createElement when imported crashes the worker before any of your code runs — the most common cause of status 15 after a migration. Replace window with globalThis, move DOM-dependent code to an offscreen document, or swap the dependency. Bundle the worker separately and run it in Node (node --check for syntax, a smoke import for top-level errors) in CI to catch this early.
5. Match syntax to your minimum Chrome version
1// esbuild / Vite: target the oldest Chrome you support
2// esbuild --target=chrome116
Execution context: the build. A worker built for the latest syntax fails on older Chrome versions with a syntax error at evaluation. Set the bundler target to your minimum_chrome_version. Users on older browsers see the registration failure as “the extension does nothing”.
6. Catch top-level errors without hiding them
1// sw.js
2try {
3 registerListeners(); // must stay synchronous
4} catch (err) {
5 console.error("[sw] top-level failure", err);
6 throw err; // keep it visible in the Errors view
7}
Execution context: the service worker’s top level. Wrapping top-level setup lets you log context — or report the error to your telemetry endpoint with a fetch — while still rethrowing so Chrome records the failure. Swallowing the exception would register a worker with no listeners, which is worse: it looks healthy and does nothing.
7. Reproduce in a clean profile
If the extension works for you but users report the failure, load the exact store build in a clean Chrome profile on the minimum supported version. Differences in Chrome version, policies that block extension features, or a corrupted profile cache (rare — fixed by reinstalling the extension) account for the remaining cases.
Common mistakes
- Loading the source folder instead of the build output. Paths and files differ.
importin a classic worker. Add"type": "module"or bundle.- DOM access in imported libraries. Crashes evaluation silently from your perspective.
- Build target newer than users’ Chrome. Syntax errors on older versions.
- Swallowing top-level errors. The worker registers but never works.
Cross-browser variation
- Chrome / Edge: status codes and the Errors view as described.
- Firefox: background scripts load in an event page; failures appear in
about:debugging→ Inspect → Console, with clear exception messages and no numeric status. - Safari: errors appear in the Develop menu’s Web Extension Background Content inspector; module workers require recent Safari versions.
Verification
- After the fix,
chrome://extensionsshows no errors and “Inspect views: service worker” is present. - Click it and confirm the console shows your startup log without exceptions.
- Stop the worker in
chrome://serviceworker-internalsand trigger an event; it restarts cleanly. - Add a CI step that evaluates the worker bundle in a worker-like environment and fails on top-level errors.
FAQ
Why does the error only appear after reload, not on install?
Both register the worker; you may simply not have looked at the Errors view after install. Errors persist until cleared.
Can a failed registration break existing users on update?
Yes — the new version’s worker fails to start and the extension stops working. Test the packaged build before publishing.
Does a runtime error in a listener cause this?
No. Listener errors happen after registration and appear as ordinary errors. Registration failures come only from top-level evaluation.
Related
- Debugging a service worker that won’t start — broader start-up debugging.
- Loading scripts with importScripts — the classic-worker alternative.
- Reading errors from the extensions page — the Errors view in detail.
- Service worker fundamentals — the parent topic.