Debug a Chrome Extension: Full Guide (2026)
Debug a Chrome Extension: Full Guide (2026)
The hardest part of learning to debug a Chrome extension is that there is no single console. An extension is three or four independent JavaScript environments running in different processes with different lifetimes, and each one has its own DevTools window. A log statement that never appears is usually not a broken log statement — it is a log statement in a context you are not looking at.
This guide maps every context, shows how to open the right inspector for each, and works through the failure modes that Manifest V3 introduced. If you have not built one yet, start with how to build a chrome extension and come back when something stops working.
The Four Contexts You Are Actually Debugging
Before any tooling, get the mental model right. When you debug a Chrome extension you are debugging up to four separate environments:
The service worker (background). Runs in its own process with no DOM, no window object, and no page. Handles events — installs, alarms, messages, command shortcuts, network interception. In Manifest V3 it is a service worker rather than a persistent background page, which is the source of most modern debugging surprises.
Content scripts. Injected into web pages. They share the page DOM but run in an isolated JavaScript world, meaning they cannot see the page variables and the page cannot see theirs. They have access to a limited slice of the extension APIs.
Extension pages. The popup, the options page, the side panel, and any full-page HTML your extension ships. Each is a real document with a real window, and each has its own lifetime — the popup in particular dies the moment it loses focus.
The page itself. Sometimes the bug is not in your code at all. A Content Security Policy on the host page, a competing extension, or a site that overrides a global can all break an extension that is otherwise correct.
Every debugging question starts with: which of these four is the code running in? Answer that first and the right tool is obvious.
Opening the Right Inspector for Each Context
Load your unpacked extension at chrome://extensions with Developer mode on. Then:
Service worker. The extension card shows a line reading "Inspect views: service worker". Click it. A DevTools window opens scoped to the worker, with Console, Sources, and Network. If the link says "service worker (inactive)", the worker has been terminated — clicking it wakes it and attaches.
Popup. Right-click the extension icon in the toolbar and choose Inspect popup. This is the only reliable way, because clicking away from the popup normally closes it. With DevTools attached and focused, the popup stays open.
Options page. Open it (right-click the icon, Options), then press F12 like any normal page. It is a document, so normal page debugging applies.
Side panel. Right-click inside the panel and choose Inspect.
Content script. Open DevTools on the host page. In the Console, use the context dropdown in the toolbar — it defaults to "top". Change it to your extension name and the console now evaluates in the isolated world where your content script lives. This dropdown is the single most missed feature in extension debugging. Without switching it, your content script variables are simply not there.
Everything at once. chrome://extensions shows an Errors button on any card that has thrown. It aggregates errors from all contexts, including ones that occurred while no inspector was attached. Check it first when something failed silently.
Manifest V3 Service Worker Gotchas
Most people who struggle to debug a Chrome extension in 2026 are hitting the service worker lifecycle, not a bug in their logic.
The worker terminates. After roughly thirty seconds of inactivity, Chrome shuts the service worker down. Every module-level variable is gone. Code that sets a counter at the top of the file and increments it on each message will silently reset. The fix is not to keep the worker alive — it is to persist state with the storage API and read it at the start of each handler.
Listeners must be registered synchronously at the top level. When the worker wakes for an event, Chrome runs the script from the beginning and then dispatches. If you register a listener inside a promise callback or after an await, registration happens too late and the event is lost. Every addListener call belongs at module scope, before anything asynchronous.
Async message handlers need an explicit return. In the onMessage listener, returning true tells Chrome to keep the message channel open for an asynchronous response. Forget it and your sendResponse fires into a closed channel. The symptom is a response that simply never arrives and no error anywhere.
Alarms replace timers. A setTimeout longer than the worker lifetime never fires, because the worker dies first. The alarms API survives termination and is the correct tool for anything scheduled.
Remote code is prohibited. Manifest V3 forbids loading and executing script from a remote URL. Bundle everything. This is the most common cause of an extension that runs fine locally and fails review or breaks after packaging.
Registration state can go stale. If the worker behaves as if it is running an older version of your code after a reload, inspect it at chrome://serviceworker-internals or remove and re-add the unpacked extension. Rare, but it happens, and chasing a phantom bug in code that is not the code running is a special kind of wasted afternoon.
Content Script Debugging: The Isolated World
Content scripts are the second-largest source of confusion when you debug a Chrome extension.
They share the DOM, not the JavaScript. Your content script can read and modify page elements. It cannot see a variable the page defined, and the page cannot call a function your script defined. Both sides see the same document and separate global scopes.
Console context switching is mandatory. In the page DevTools, the Console toolbar has a dropdown listing execution contexts. Selecting your extension puts the console in the isolated world. Then your variables exist, your functions are callable, and chrome.runtime is defined. Leave it on "top" and none of that is true.
Breakpoints live under Content scripts in Sources. The Sources panel has a Content scripts tab in the file navigator alongside Page. Your injected files are there, and breakpoints set there behave normally.
Injection timing matters. The run_at field controls whether your script runs at document_start, document_end, or document_idle. A script that queries for an element that does not exist yet returns null and fails quietly. If your selector works when pasted in the console but not in your script, timing is the first suspect.
Programmatic injection reports differently. Scripts injected with the scripting API from the service worker do not appear in the manifest and can be harder to spot in Sources. A debugger statement at the top of the injected function is the fastest way to catch execution.
The host page CSP can block you. Some sites ship a strict Content Security Policy. Content scripts are generally exempt for their own execution, but anything you inject into the page context — a script tag, an inline handler — is subject to the page policy. The console error will name the CSP directive that blocked it.
Debugging Permissions, Storage, and Messaging
Three subsystems with three distinct failure signatures.
Permissions. When an API returns undefined or throws about an unknown property, the permission is usually missing from the manifest. Chrome does not always produce a loud error — it sometimes just gives you an object without the method you expected. Check the Permissions section on the extension card in chrome://extensions against what your code calls. For optional permissions, remember they are requested at runtime and can be revoked by the user at any time, so the code path where the permission is absent needs to actually work.
Host permissions and site access. An extension with "On click" site access will not run its content script until the user activates it. During development your extension may be set to "On all sites" while your users are on the default. Test both. This asymmetry produces bug reports that are impossible to reproduce locally.
Storage. The DevTools Application panel, under Storage, shows extension storage when you have the extension context inspected. The local and sync areas behave differently: sync has strict per-item and total quotas and rate limits on writes, so an extension that writes on every keystroke will start silently failing quota checks. Watch for the runtime lastError value after a set call — asynchronous storage failures do not throw.
Messaging. Message passing failures are almost always one of three things: the receiving context is not alive, the listener was registered too late, or the async response channel was not kept open. Log at both ends before assuming the message is malformed. When the receiving end is a terminated service worker, Chrome wakes it — but only if the listener was registered synchronously, which loops back to the earlier rule.
The Reload Rules Nobody Documents Clearly
Knowing exactly what needs reloading saves more time than any breakpoint, because half the time you spend trying to debug a Chrome extension is spent looking at code the browser never loaded.
- Manifest changes — click the reload arrow on the extension card. Always required.
- Service worker changes — click the reload arrow. The old worker is torn down and the new script installed.
- Content script changes — click the reload arrow, then reload the host page. Both. Reloading only the extension leaves the old script running on already-open tabs.
- Popup and options HTML or CSS — no reload needed. The document is re-read each time it opens.
- Popup or options JavaScript — usually re-read on open, but click the extension reload arrow if you see stale behavior.
- Anything in a packed installed version — you cannot hot-reload it. Load the unpacked build for development.
A file watcher that calls the reload endpoint is worth setting up on any extension you work on for more than a week. Without one, "did I reload it?" becomes a permanent background question and you will eventually spend twenty minutes debugging code that was never loaded.
Testing Keyboard Commands and User-Facing Behavior
Extensions that expose keyboard shortcuts have a debugging surface people forget entirely.
Check the binding actually exists. Visit chrome://extensions/shortcuts. Every command your manifest declares appears there. Commands with no assigned key show as blank — and Chrome will not assign a suggested key if another extension already claimed it. A command that "does not fire" frequently has no key bound at all.
Suggested keys are suggestions. The suggested_key field in the manifest is a request. Chrome honors it only when the combination is free, and the user can rebind anything. Never assume your suggested binding is the live one.
The command fires in the service worker. The onCommand listener is a background event. If your handler is in the popup, it will never run, because the popup does not exist when the user presses the key.
Test with the popup closed and the worker asleep. This is the real user condition. A command that works while you have the service worker inspector open may fail cold, because having DevTools attached keeps the worker alive. Close the inspector, wait, then press the key.
That last point matters for any extension whose entire value is a hotkey. A single-purpose tool like the Ctrl+Shift+C extension — one command, copy the active tab address to the clipboard, no network calls and no data collection — has exactly one code path that must work cold, every time, with the worker asleep. Narrow surface, and correspondingly narrow debugging: check the binding at the shortcuts page, check the command listener is top-level, check the clipboard write succeeds in the right context. Extensions with dozens of features do not get to be that simple. If you want to see the shape of a minimal command extension in use, copy url chrome extension describes the user-facing side of it.
Reproducing Bugs You Cannot See
The hardest bugs to debug in a Chrome extension are the ones only your users hit.
Ask for the Errors panel contents. Users can open chrome://extensions, click Errors on your card, and copy the output. It is the closest thing to a crash report the platform offers.
Reproduce on a clean profile. Other extensions conflict more often than people expect — two extensions injecting into the same page, or claiming the same keyboard shortcut. A fresh Chrome profile with only your extension installed isolates this in a minute.
Test the packed build. Package the extension as a CRX or install it from an unlisted Web Store draft. Path resolution, CSP, and remote-code restrictions all behave differently packed versus unpacked.
Ask which Chrome channel they are on. Stable, Beta, Dev, and Canary can differ in extension API behavior, and knowing the channel narrows the search before you try to debug a Chrome extension issue you cannot reproduce.
Test on the oldest Chrome you support. Extension APIs move. A method available in the current stable may not exist for a user two versions behind.
Test with site access set to "On click". Most users never change the default, and the default is not what you have been developing against.
Add structured logging behind a flag. A debug mode that writes timestamped entries to storage, dumpable from the options page, turns "it sometimes does not work" into an actual timeline. Ship it disabled.
For the DevTools techniques that apply to all of this beyond extension specifics, chrome devtools tips and tricks covers conditional breakpoints, logpoints, and snippets, all of which apply directly to extension code.
Frequently Asked Questions
How do I open the console for a Chrome extension service worker?
Go to chrome://extensions, enable Developer mode, and click the "service worker" link in the Inspect views line on your extension card. A DevTools window opens scoped to the worker. If it reads "(inactive)", clicking it wakes the worker and attaches the inspector.
Why do my content script logs not appear in the extension console? Content scripts run in the page process, not the extension process. Open DevTools on the host page, then switch the Console context dropdown from "top" to your extension name. Your logs and variables live in that isolated world.
Why does my service worker keep stopping? By design. Manifest V3 terminates idle service workers to save resources, and everything held in module-level variables is lost. Persist anything that must survive to the storage API and read it at the start of each handler rather than trying to keep the worker alive.
How do I debug a Chrome extension popup that closes when I click away? Right-click the extension icon and choose Inspect popup. The attached DevTools window keeps the popup alive while it is focused, so you can inspect the DOM, step through code, and watch network requests.
Do I need to reload the extension after every change? Manifest, service worker, and content script changes all require clicking the reload arrow on the extension card, and content script changes also require reloading the host page. Popup and options markup and styles are re-read whenever the page opens.
Where do I see errors that happened while the extension was not open?
The Errors button on the extension card in chrome://extensions. It aggregates throws from every context, including ones that happened with no inspector attached, which makes it the right first stop for silent failures.
Why does my extension work unpacked but break after publishing? Usually a permission that behaved differently during local development, a path that resolved only in the unpacked layout, or a reference to remotely hosted code that Manifest V3 rejects in a packaged build. Test a packed build before submitting.
Debug the Context, Not the Code
Nearly every hour lost trying to debug a Chrome extension is spent looking at the wrong console. Identify the context first — service worker, content script, extension page, or host page — open the inspector that belongs to it, and the bug usually becomes ordinary JavaScript debugging within a minute. Then respect the Manifest V3 rules: top-level listeners, persisted state, no remote code, explicit async responses. If you want a working reference for how small a well-scoped extension can be, Ctrl+Shift+C is a single command with clipboard permission only, no network calls, and zero data collection — the entire surface fits in one service worker file. Install it, inspect it, and use it to paste your reproduction links into tickets in one keystroke.
Try Ctrl+Shift+C
Copy any URL with one keyboard shortcut. Free forever, no data collected.