Finding the Right DevTools Target for Each Context

Open the correct inspector for every MV3 context — service worker, popup, options, side panel, offscreen document, content script and DevTools panel — and avoid debugging the wrong one.

Published September 18, 2026 Updated September 18, 2026 7 min read
Table of Contents

An extension is five or six separate JavaScript environments, and each has its own DevTools. A console.log in the service worker never appears in the page’s console; a breakpoint set in the popup’s inspector does nothing for the options page; and opening the worker’s DevTools changes the very behaviour you are trying to debug, because an attached inspector keeps the worker alive. Half of extension debugging is knowing which window to look in. This guide is part of debugging extension contexts.

Where each context’s inspector lives

Opening DevTools for each extension contextService worker, popup, options page, side panel, offscreen document, content script and DevTools panel mapped to how to open their inspector and the side effect of doing so.ContextHow to openSide effectService workerchrome://extensions → service workerWorker never evictedPopupRight-click action → Inspect popupPopup stays openOptions / tab pageNormal DevTools on that tabNoneSide panelRight-click inside → InspectNoneOffscreen documentchrome://inspect/#otherNoneContent scriptPage DevTools, context dropdownNoneDevTools panelRight-click in panel → InspectNone
Two of these change behaviour when inspected — the worker stops being evicted and the popup stops closing.

Step-by-step

1. Inspect the service worker, and remember what that does

On chrome://extensions, enable Developer mode, find your extension and click the service worker link. That opens a DevTools window attached to the worker.

1// Paste into the worker's console to confirm you are in the right place
2({ scope: self.registration?.scope, hasDocument: typeof document !== "undefined" });
3// { scope: "chrome-extension://abc…/", hasDocument: false }

Execution context: the service worker’s console. hasDocument: false is the quickest confirmation. While this window is open, Chrome will not evict the worker — so timer bugs, cold-start races and anything described in alarms vs setTimeout in service workers will not reproduce. Close it to test eviction; reopen it afterwards to read persisted logs.

2. Inspect the popup without it vanishing

Right-click the toolbar icon and choose Inspect popup. The popup opens and stays open for as long as its DevTools is attached.

1location.pathname;              // "/popup.html"
2document.visibilityState;       // "visible"

Execution context: the popup’s console. The popup now behaves differently from normal use — it will not close on blur — so test close-related behaviour, such as the pagehide flush described in why the popup closes and how to work with it, with the inspector closed.

3. Find a content script’s console

A content script’s logs appear in the page’s DevTools, but its variables live in a separate world. Open DevTools on the page, go to Console, and switch the context dropdown (top left, usually “top”) to your extension’s name.

1// With the extension's context selected:
2typeof chrome.runtime?.id;     // "string" — you are in the isolated world
3// With "top" selected:
4typeof chrome?.runtime?.id;    // "undefined" — you are in the page's main world

Execution context: the page’s DevTools console, in either the main world or your isolated world depending on the dropdown. Typing a variable your content script defined while “top” is selected returns undefined, which is the single most common source of “my variable is not set” confusion — covered further in debugging content scripts in the isolated world.

4. Reach the contexts without a visible window

An offscreen document has no window to right-click. Open chrome://inspect/#other and look for your extension’s offscreen URL; click inspect.

1// Confirm it exists before hunting for it
2await chrome.runtime.getContexts({ contextTypes: ["OFFSCREEN_DOCUMENT"] });
3// [{ contextType: "OFFSCREEN_DOCUMENT", documentUrl: "chrome-extension://…/offscreen/host.html" }]

Execution context: the service worker’s console. If the list is empty, the document is not open — it may have closed itself after going idle, as in creating and closing offscreen documents.

5. Enumerate every context at once

chrome.runtime.getContexts lists everything the extension currently has open, which is the fastest way to answer “is my side panel actually running?”.

1(await chrome.runtime.getContexts({})).map((c) => `${c.contextType} ${c.documentUrl ?? ""} tab=${c.tabId}`);
2// ["BACKGROUND chrome-extension://…/sw.js tab=-1",
3//  "POPUP chrome-extension://…/popup.html tab=-1",
4//  "SIDE_PANEL chrome-extension://…/panel.html?tab=42 tab=42"]

Execution context: the service worker’s console, Chrome 116+. Content scripts are not listed — they run inside pages rather than as extension contexts — so this shows only extension-origin documents.

Where console output goesEach extension context writes to its own console: the worker to its inspector, pages to theirs, and content scripts to the host page's console under the extension's context.Service worker consolechrome://extensions → service workerbackground logic, alarms, messagesExtension page consolespopup, options, side panel, offscreenone per documentHost page consolecontent script context selectedshared with the page's own logschrome://extensions → Errorsuncaught errors, manifest problemscollected across contexts
No console aggregates them — which is why a shared logging helper is worth writing.

Debugging without changing the thing you debug

The observer effect is the defining difficulty of extension debugging: the two contexts most likely to have lifecycle bugs — the worker and the popup — are exactly the two whose lifecycle DevTools alters. Three techniques work around it.

Log to storage, read later. A tiny helper that appends to a capped list in chrome.storage.local lets you run the extension with no inspector attached, reproduce the bug, and then open any extension page to read what happened.

Use chrome://extensions → Errors. Uncaught errors from every context are collected there with a timestamp and stack, whether or not DevTools was open at the time.

Reproduce on Safari or with the worker stopped. Clicking Stop on the worker in chrome://extensions, then triggering the event, produces a genuine cold start with no inspector attached.

1// A minimal storage-backed logger, shared by every context
2export async function trace(ctx, ...args) {
3  const line = `${new Date().toISOString()} [${ctx}] ${args.map(String).join(" ")}`;
4  const { traceLog = [] } = await chrome.storage.local.get("traceLog");
5  traceLog.push(line);
6  await chrome.storage.local.set({ traceLog: traceLog.slice(-200) });
7}

Execution context: any extension context, including content scripts. It is deliberately simple; the fuller pattern with batching and levels is in logging across contexts without losing messages.

Debugging a lifecycle bug without the observer effectClose all inspectors, stop the worker, reproduce the bug, then open an extension page to read the storage-backed trace and the errors page.Close every inspectorworker, popupStop the workerchrome://extensionsReproduceas a user wouldthen read what was recordedtraceLog in storagefrom any extension pageErrors pageuncaught, all contextsNow attach DevToolsto inspect state
Open DevTools only after the bug has happened — to read what was recorded, not to watch it happen.

Cross-browser variation

  • Chrome / Edge: the targets above. chrome://inspect/#other lists offscreen documents and other hidden extension pages. getContexts from Chrome 116.
  • Firefox: about:debugging#/runtime/this-firefox → your extension → Inspect opens a toolbox for the background page with a frame picker covering the popup and other extension pages. The popup can be kept open with the “Disable popup auto-hide” option in the toolbox menu.
  • Safari: enable the Develop menu; extension background pages and popups appear under Develop → Web Extension Background Content and the popup’s own entry. Content scripts are debugged from the page’s Web Inspector.
  • All three: an attached inspector keeps background contexts alive. Always verify lifecycle fixes with the inspector closed.

Verification

  1. Add a trace("sw", "started") at the top of the worker and a trace("popup", "opened") in the popup. Stop the worker, open the popup, then read the log from the options page:
1(await chrome.storage.local.get("traceLog")).traceLog.slice(-5);
2// ["…Z [sw] started", "…Z [popup] opened"]

Execution context: the options page console. Both lines present, in order, confirms each context ran and that you can observe them without inspectors attached.

  1. Switch the page console’s context dropdown between “top” and your extension and confirm a content-script variable is visible only in the latter.
  2. Run getContexts({}) with the side panel open and closed and confirm it appears and disappears.
  3. Trigger an uncaught error in the popup and confirm it appears on the Errors page.

FAQ

Why do my worker logs disappear?

The worker’s DevTools shows logs only while it is attached; a log written while the worker ran unobserved is gone once the worker is evicted. Persist the logs you care about.

Can I debug the service worker without keeping it alive?

Not with an attached inspector. Use storage-backed logs or the Errors page, and attach DevTools only to inspect state after the fact.

Where do errors from executeScript functions show up?

In the target page’s console under your extension’s context, and in the result array’s error field returned to the caller.

Other Testing, Debugging & Performance Optimization Resources