Development13 min read

Debugging Chrome Extensions Like a Pro

Master Chrome extension debugging with DevTools. Debug service workers, content scripts, popups, storage, and network requests with practical techniques and code examples.

C
CWS Kit Team
Share
🔍

Debugging Chrome Extensions Like a Pro

Stop guessing. Start inspecting.

Extension debugging is different from web debugging. You are not working with one context — you are juggling multiple isolated JavaScript environments that communicate through message passing. The popup has its own DevTools. The background service worker has its own DevTools. Each content script runs in its own isolated world on each page. And errors in one context can silently affect behavior in another.

Most extension bugs are not hard to fix. They are hard to find. This guide gives you a systematic approach to locating bugs across every execution context, using the tools Chrome provides.

Debugging by context#

Each part of a Chrome extension runs in a different environment with different capabilities and different debugging access points. Pick the tab that matches where your bug lives.

Right-click your extension icon and select 'Inspect popup.' This opens DevTools scoped to the popup's DOM, JavaScript, and network requests. The catch: if you click away from the popup, it closes and DevTools disconnects. To keep it open while debugging, undock DevTools into a separate window (click the three-dot menu → undock into separate window) BEFORE clicking outside the popup. Common popup bugs: state lost on close (the popup re-renders from scratch each time it opens), event listeners not attached (DOMContentLoaded fires before your script if the script tag lacks 'defer'), and CSS not applying (the popup has its own document, not the page's stylesheet).

Systematic debugging process#

When something is not working and you do not know why, resist the urge to add random console.log statements. Follow a structured process.

1

Reproduce consistently

Before debugging, find exact steps to reproduce the bug every time. If it is intermittent, note the conditions: which page, which state, how long after install. Intermittent bugs in extensions are almost always related to service worker lifecycle or race conditions in message passing.

2

Identify the execution context

Determine which part of the extension is misbehaving. Is the popup not rendering? Is the content script not injecting? Is the background not responding to messages? Each context has its own DevTools instance. Debugging the wrong context wastes time.

3

Check the error console first

Open chrome://extensions and look for 'Errors' on your extension card. This catches errors across ALL contexts. Then check the specific DevTools console for the affected context. Many extension bugs produce errors that developers never see because they are looking at the wrong console.

4

Verify permissions and manifest

If a Chrome API call fails silently, the most common cause is a missing permission in manifest.json. Check that every API you use is declared. Check that host_permissions cover the domains your content scripts target. Use chrome.runtime.lastError after every async API call.

5

Isolate with breakpoints

Set breakpoints in the Sources panel rather than using console.log. Breakpoints let you inspect the full scope, call stack, and closure variables at the exact moment of failure. Conditional breakpoints (right-click → 'Add conditional breakpoint') are invaluable for debugging loops.

6

Test in a clean profile

If the bug is not reproducible in a clean Chrome profile, another extension is interfering. Create a new profile (chrome://settings → Profiles → Add), install only your extension, and test. Extension conflicts are more common than most developers expect.

Common error messages and fixes#

These are the extension errors developers encounter most frequently, along with the actual causes and solutions.

Essential debugging techniques#

Console filtering#

When debugging content scripts, the page's console is shared between your code and the page's own JavaScript. Use console filtering to see only your output.

// Prefix all your console output with a tag
const log = (...args: unknown[]) => console.log('[MyExtension]', ...args);
const warn = (...args: unknown[]) => console.warn('[MyExtension]', ...args);
const error = (...args: unknown[]) => console.error('[MyExtension]', ...args);
 
// Usage
log('Content script initialized on', window.location.href);
warn('Element not found, retrying in 500ms');

In DevTools Console, type [MyExtension] in the filter box. Now you see only your extension's output, even on pages with hundreds of their own console messages.

Network monitoring for extensions#

Service worker network requests do not appear in the page's Network tab. You need to inspect the service worker's own DevTools.

// Add request/response logging to your service worker
const originalFetch = globalThis.fetch;
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
  const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
  console.log(`[Fetch] ${init?.method || 'GET'} ${url}`);
  const startTime = performance.now();
 
  try {
    const response = await originalFetch(input, init);
    const duration = Math.round(performance.now() - startTime);
    console.log(`[Fetch] ${response.status} ${url} (${duration}ms)`);
    return response;
  } catch (err) {
    console.error(`[Fetch] FAILED ${url}`, err);
    throw err;
  }
};

Storage inspector#

The Application tab in DevTools has an "Extension Storage" section (under Storage) that lets you view and edit chrome.storage.local and chrome.storage.sync values in real time. This is faster than writing console commands.

For programmatic inspection during debugging, add a helper to your service worker:

// Dump all storage contents to console
async function debugStorage() {
  const local = await chrome.storage.local.get(null);
  const sync = await chrome.storage.sync.get(null);
  const session = await chrome.storage.session.get(null);
 
  console.group('Extension Storage Debug');
  console.log('Local:', JSON.stringify(local, null, 2));
  console.log('Sync:', JSON.stringify(sync, null, 2));
  console.log('Session:', JSON.stringify(session, null, 2));
  console.log('Sync bytes used:', await chrome.storage.sync.getBytesInUse(null), '/ 102400');
  console.groupEnd();
}
 
// Call from console: debugStorage()
// Or attach to a debug keyboard shortcut

Message passing debugging#

Message passing bugs are the most common source of "it works sometimes" behavior. Add tracing to both sides:

// In your service worker — log all incoming messages
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  console.log('[MSG IN]', {
    type: message.type,
    data: message,
    from: sender.tab ? `Tab ${sender.tab.id}: ${sender.tab.url}` : 'Extension',
    frameId: sender.frameId,
  });
 
  // Your actual handler logic here
  // IMPORTANT: return true if sendResponse will be called asynchronously
  return true;
});
 
// In your content script — log outgoing messages and responses
async function sendMessage<T>(message: { type: string; [key: string]: unknown }): Promise<T> {
  console.log('[MSG OUT]', message);
  try {
    const response = await chrome.runtime.sendMessage(message);
    console.log('[MSG RESP]', response);
    return response as T;
  } catch (err) {
    console.error('[MSG ERR]', err);
    throw err;
  }
}

Breakpoints in content scripts#

To set breakpoints in content scripts:

  1. Open DevTools on the page (F12)
  2. Go to Sources panel
  3. In the left sidebar, expand "Content scripts"
  4. Find your extension's ID folder
  5. Open the content script file
  6. Click the line number to set a breakpoint

For conditional breakpoints that only trigger on specific pages or elements:

// Instead of plain breakpoints, use programmatic debugger statements
// with conditions — easier to manage than DevTools conditional breakpoints
 
function processElement(el: HTMLElement) {
  // This breakpoint only triggers for elements with the target class
  if (el.classList.contains('debug-target')) {
    debugger; // DevTools will pause here if open
  }
  // ... rest of your logic
}

Chrome extension debugging flags#

Chrome has hidden flags that make extension development easier. Navigate to chrome://flags and enable these:

  • Extensions Developer Mode: Already available on chrome://extensions — enables loading unpacked extensions, shows errors, and provides the "Update" button for reloading.
  • Enable extension service worker keepalive: Under chrome://flags/#enable-extension-service-worker-keepalive — extends the 30-second timeout during debugging. Useful for testing but do not rely on it for production behavior.

On the chrome://extensions page itself:

  • Developer mode toggle (top right): Must be on for unpacked extensions and shows the "Errors" button per extension.
  • Update button: Forces all extensions to reload their service workers and re-inject content scripts. Faster than removing and re-adding the extension.
Check chrome://extensions errors first

Before opening DevTools, before adding console.log, before anything else — check the Errors button on your extension's card at chrome://extensions. This surface catches errors from ALL contexts (service worker, content scripts, popup) in one place. More than half of extension bugs can be diagnosed from this screen alone: missing permissions, syntax errors, lifecycle failures, and API deprecation warnings.

Debugging performance issues#

Slow extensions frustrate users and can trigger Chrome's performance warnings. Use these techniques to find bottlenecks.

Content script performance: If your content script makes pages slow, open DevTools Performance tab and record a page load. Your content script's execution time appears in the "Main" thread flame chart. Filter for your extension ID. If it takes more than 50ms, optimize by deferring non-critical work with requestIdleCallback().

Service worker startup time: Service workers that take too long to start cause delayed responses to user actions. Measure startup time by adding performance.mark('sw-start') at the top of your service worker and performance.mark('sw-ready') after all listeners are registered. Use performance.measure('sw-init', 'sw-start', 'sw-ready') to see the duration.

Storage read performance: chrome.storage.local.get() can be slow if you are storing large objects. Instead of one key with a huge JSON blob, split data into multiple keys and read only what you need. Reading a specific key is O(1); reading null (all keys) is O(n).

// Slow: reads everything, even if you only need one setting
const all = await chrome.storage.local.get(null);
const theme = all.settings?.theme;
 
// Fast: reads only the specific key
const { theme } = await chrome.storage.local.get('theme');

For a deeper understanding of service worker architecture that affects debugging, read our guide on background service workers deep dive. And for permission-related debugging, see Manifest V3 permissions best practices.

Build debugging into your extension from day one

Add a debug mode toggle to your extension that enables verbose logging, storage inspection, and message tracing. Ship it in production behind a hidden flag (e.g., holding Shift while clicking the extension icon). When a user reports a bug, you can ask them to enable debug mode and send you the console output. This single practice will cut your debugging time in half.

Continue reading

Related articles

View all posts