Debugging Extension Contexts in MV3

Debug every MV3 context systematically: service worker via chrome://extensions Inspect, content scripts in page DevTools, popup and options via right-click inspect, source maps and the Errors panel.

MV3 splits extension code across at least three isolated contexts — the service worker, content scripts, and extension pages (popup, options, sidepanel) — and each one requires a different DevTools entry point. The failure mode that confuses most developers is opening the wrong DevTools for the wrong context: looking for service worker logs in the popup console, or trying to inspect a content script variable from the extension’s background page. Nothing is in scope. This guide is part of the Testing, Debugging & Performance Optimization section and maps each context to its correct inspection path, covering source map configuration, cross-context logging patterns, and the Errors panel that surfaces registration failures before any DevTools is open.

MV3 context debugging map: four contexts, four DevTools entry pointsEach MV3 execution context — service worker, content script, popup, and options page — maps to a distinct DevTools opening path. The Errors panel on chrome://extensions catches registration failures before any context starts.chrome://extensions → Errors panelCatches registration failures before any context is aliveService WorkerbackgroundInspect link →Content Scriptisolated worldPage DevTools →Popupextension pageRight-click Inspect →Options Pageextension pageRight-click Inspect →SW DevToolsisolated SW panelPage DevToolsSources → content scriptsExtension Page DevToolsfull panel, chrome-extension:// origin

Prerequisites checklist

  • Developer mode enabled on chrome://extensions — without it the Inspect link is hidden and the Errors panel is not accessible.
  • Source maps configured in your bundler output — without them every stack trace points at minified output with no useful line numbers.
  • Extension loaded as unpacked — a CRX-installed extension from the Web Store runs in a read-only bundle and its source is not inspectable via DevTools.
  • No eval() or Function() constructor in service worker code — Chrome’s extension CSP blocks inline evaluation, and any attempt produces a CSP error that masks the real problem you are investigating.

1. Debugging the service worker

The service worker is not a page — it has no DOM and does not appear in the browser’s window list. Its DevTools instance is accessed through chrome://extensions.

Navigate to chrome://extensions, find your extension card, and click the Service worker link (shown as “Inspect views: Service Worker” or a blue “service worker” hyperlink). This opens a dedicated DevTools window attached to the worker’s V8 context.

 1// background.ts — structured logging that survives source-map-less stacks
 2const LOG_PREFIX = '[SW]';
 3
 4function log(event: string, data?: unknown) {
 5  console.log(`${LOG_PREFIX} ${event}`, data ?? '');
 6}
 7
 8chrome.runtime.onInstalled.addListener(details => {
 9  log('onInstalled', details.reason);
10});
11
12chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
13  log('onMessage', { msg, tabId: sender.tab?.id });
14  // always return true if response is async
15  return true;
16});

Execution context: Service worker background context. console.log output appears only in the dedicated SW DevTools window opened via the chrome://extensions Inspect link — it does not appear in the page’s DevTools or in the popup’s console. Structured log prefixes like [SW] make it possible to filter output when you have many console lines streaming in.*

After opening SW DevTools, the Network tab shows all fetch() calls the worker makes (including calls to extension APIs that hit Chrome’s internal network layer), and the Application → Service Workers panel shows registration state, scope, and whether the worker is running or idle.

2. Debugging content scripts

Content scripts run in the host page’s process but in an isolated JavaScript world. They appear in the page’s DevTools — not the extension’s — under Sources → Content scripts. Open DevTools on the host page (F12 or right-click → Inspect), then navigate to the Sources panel and expand the Content scripts tree on the left.

1// content.ts — identify yourself immediately for fast filtering
2console.log('[ContentScript] active on', location.href, 'at', Date.now());
3
4// Listen to messages from the service worker
5chrome.runtime.onMessage.addListener((msg) => {
6  console.log('[ContentScript] message received', msg);
7});

Execution context: Content script isolated world. The script has access to the extension’s chrome.* APIs and the host page’s DOM but not to the page’s JavaScript variables. When you set a breakpoint inside Sources → Content scripts, Chrome pauses in the isolated world — the page’s own scripts continue executing. On Firefox, content scripts appear under the Debugger panel in the same way; on Safari, they appear under the Develop menu’s Web Inspector.*

A common debugging trap: if you use window.addEventListener inside a content script to communicate with page scripts via custom events, that DOM boundary is visible in DevTools — page-world scripts appear in the main Sources tree while the isolated-world content script appears in the Content scripts subtree.

3. Debugging popup and options pages

Extension pages (popup, options page, new-tab override) each get their own DevTools instance. The popup is the trickier case because it closes when it loses focus, taking its DevTools window with it.

For the popup: right-click the extension icon in the toolbar and choose Inspect popup (Chrome). This pins the popup open and opens DevTools attached to its page context simultaneously. Alternatively, open the popup HTML directly at its chrome-extension:// URL and use normal F12 DevTools.

For the options page: navigate to chrome://extensions, click the extension’s Details button, then click Extension options. The options page opens as a tab — press F12 to open standard DevTools.

1// popup.ts — always read storage async; never assume the worker has already written
2document.addEventListener('DOMContentLoaded', async () => {
3  console.log('[Popup] DOM ready, reading storage');
4  const { theme } = await chrome.storage.local.get('theme');
5  console.log('[Popup] theme =', theme);
6  document.body.dataset.theme = theme ?? 'light';
7});

Execution context: Extension page (popup HTML). The popup’s JS context is created fresh every time the popup opens and destroyed when it closes. Any in-memory state is lost on close — always persist to chrome.storage before the user dismisses the popup. The chrome.* APIs are fully available here without any permission bridge.*

4. Source maps for readable stack traces

Without source maps, every error stack trace points at your bundler’s output, making line numbers useless. For Vite and Webpack the configuration is a one-liner:

 1// vite.config.ts
 2import { defineConfig } from 'vite';
 3
 4export default defineConfig({
 5  build: {
 6    sourcemap: true,           // emits .js.map files alongside output
 7    rollupOptions: {
 8      input: {
 9        background: 'src/background.ts',
10        content:    'src/content.ts',
11        popup:      'src/popup/index.ts',
12      },
13    },
14  },
15});

Execution context: Build tool configuration (Node.js). Setting sourcemap: true emits inline or external source maps that Chrome’s DevTools loads automatically when the extension is loaded unpacked. Do not ship source maps in production CRX packages — they expose your full source code to anyone who downloads the extension. Use a build flag to toggle sourcemap: false for release builds.*

Once source maps are active, breakpoints set in your TypeScript source files in DevTools are respected — Chrome resolves them through the map file and pauses at the correct original line.

5. The Errors panel

The Errors panel on chrome://extensions is the first place to check when an extension fails silently. It surfaces errors that occur before any context is alive — manifest parsing failures, missing file references, CSP violations during load, and uncaught exceptions at the top level of the service worker before any listener is registered.

Click Errors on the extension’s card. Each entry shows the source URL (e.g., chrome-extension://<id>/background.js), the line number (matching your source map if active), and the full error message. The Clear all button resets the count without reloading the extension.

The Errors panel captures errors in all extension contexts — service worker, content scripts, extension pages — not just the background. It is especially useful for content script CSP violations on specific host pages, which do not appear in the SW DevTools console at all.

For a focused deep-dive on service worker startup failures specifically, see debugging a service worker that won’t start.

6. Cross-context logging pattern

Because each context has its own console, a unified logging approach routes all debug output through a single channel — the service worker console — using chrome.runtime.sendMessage:

1// shared/debug-log.ts — imported by popup, content scripts and options page
2export function remoteLog(level: 'log' | 'warn' | 'error', ...args: unknown[]) {
3  chrome.runtime.sendMessage({ type: '__DEBUG_LOG__', level, args }).catch(() => {
4    // Fallback if the service worker is not yet running
5    console[level]('[remote-log fallback]', ...args);
6  });
7}
1// background.ts — receives and re-emits from all contexts
2chrome.runtime.onMessage.addListener((msg) => {
3  if (msg.type !== '__DEBUG_LOG__') return;
4  console[msg.level as 'log' | 'warn' | 'error']('[from context]', ...msg.args);
5});

Execution context: The sender runs in the popup, content script, or options page. The receiver runs in the service worker. This pattern centralises all debug output in the SW DevTools console, which persists longer than the popup or options page context. Disable or tree-shake the remoteLog calls in production builds — the extra IPC adds latency to every logged call.*

MV3 Constraints box

  • Service worker DevTools closes on worker eviction: the attached DevTools window becomes unresponsive after ~30 seconds of idle. Re-open it via the chrome://extensions Inspect link; the new session resumes with a fresh worker.
  • Popup DevTools closes on popup dismiss: clicking outside the popup closes both. Use the chrome-extension:// URL approach for stable popup debugging sessions.
  • Content scripts cannot be paused without the host page’s DevTools: a breakpoint in a content script requires DevTools open on the specific host page tab where that script is running.
  • No eval() in extension pages or service workers: the default extension CSP blocks it. debugger; statements work as a replacement for inline evaluation.
  • chrome://extensions Errors panel does not persist across browser restarts: errors are cleared when Chrome closes.

Cross-browser notes

  • Chrome — The reference platform. All paths described above (chrome://extensions, Inspect link, right-click Inspect popup) are Chrome-specific.
  • Firefox — Extension debugging goes via about:debugging#/runtime/this-firefox. Click Inspect next to your extension. The resulting DevTools panel includes a combined console that shows output from all extension contexts simultaneously, which is more convenient than Chrome’s per-context model.
  • Safari — Use the Develop menu → Web Extension Background Content to attach to the service worker equivalent. Content scripts are debuggable via Develop → Show Web Inspector on the host page. Safari requires the extension to have an explicit entitlement for debugging in production builds.