Debugging Chrome Extensions: Tips, Tools, and Techniques

15 min read

Debugging Chrome Extensions: Tips, Tools, and Techniques

Debugging Chrome extensions presents unique challenges compared to regular web applications. Extensions run in multiple isolated contexts—background service workers, content scripts, popups, and options pages—each requiring different debugging approaches. This tutorial covers essential techniques, tools, and strategies to efficiently diagnose and fix extension issues.

What You’ll Learn


Setting Up Your Debugging Environment

Before debugging, configure Chrome for extension development:

  1. Open chrome://extensions/ in Chrome
  2. Enable Developer mode using the toggle in the top-right corner
  3. For your extension, note the available debug links:
    • service worker - Opens DevTools for background worker
    • Inspect views - Options for popup, options page, or side panel
    • Errors - Shows runtime errors

Screenshot: The chrome://extensions page with Developer mode enabled, showing an extension with “service worker” link, “Inspect views” dropdown, and error count badge visible.


Chrome DevTools for Extensions

Chrome DevTools provides specialized panels for extension debugging.

Accessing Extension Contexts

Context How to Access
Service Worker Click “service worker” link on chrome://extensions
Popup Right-click extension icon → “Inspect popup”
Options Page Right-click extension icon → “Manage extension” → click the link
Content Script Open DevTools on target page → dropdown → select extension

Key DevTools Panels

Screenshot: DevTools Sources panel showing extension files with breakpoints set in a service worker.


Inspecting Background Service Workers

Background service workers are the heart of Manifest V3 extensions. They handle events, message passing, and long-running tasks.

Opening Service Worker DevTools

# Method 1: From chrome://extensions
chrome://extensions/ → Find your extension → Click "service worker"

The DevTools window shows the service worker console and sources. Look for:

Debugging Service Worker Lifecycle

// background.js - Add lifecycle logging
console.log('[SW] Service worker starting...');

self.addEventListener('install', (event) => {
  console.log('[SW] Install event:', event);
  // Skip waiting to activate immediately
  self.skipWaiting();
});

self.addEventListener('activate', (event) => {
  console.log('[SW] Activate event:', event);
  // Claim all open tabs
  event.waitUntil(self.clients.claim());
});

self.addEventListener('message', (event) => {
  console.log('[SW] Message received:', event.data);
});

Preserving Logs Across Restarts

Service workers terminate after 30 seconds of inactivity. Enable log preservation:

  1. In DevTools Console tab, click ⚙️ (Settings)
  2. Check Preserve log

This ensures you see logs from the moment the worker starts, even after restarts.

Screenshot: Console panel with “Preserve log” enabled, showing service worker lifecycle events.

Debugging Service Worker Termination

If breakpoints aren’t hitting, the worker may have terminated. Keep it alive:

// background.js - Keep service worker alive
chrome.alarms.create('keep-alive', { periodInMinutes: 0.5 });

chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'keep-alive') {
    console.log('[SW] Keep-alive ping');
  }
});

Debugging Content Scripts

Content scripts run in the context of web pages, isolated from both the page and the extension.

Finding Your Content Script

  1. Open DevTools (F12) on a page where your content script runs
  2. In the debugger dropdown (top-left), find your extension name
  3. Select your content script from the list

Screenshot: DevTools debugger dropdown showing extension content scripts alongside page scripts.

Debugging in the Correct Context

The page context and content script context are different:

// ❌ This runs in page context, not your content script
const elements = document.querySelectorAll('.my-extension');

// ✅ Switch to your extension's context first
// In DevTools console, select your extension from dropdown
const elements = document.querySelectorAll('.my-extension');
console.log('Found elements:', elements);

Handling DOM Changes

Content scripts often manipulate the page DOM. Debug effectively:

// content.js - Log DOM manipulation
function injectUI() {
  const container = document.createElement('div');
  container.id = 'my-extension-root';
  container.innerHTML = '<button>Click me</button>';
  
  document.body.appendChild(container);
  console.log('[Content] UI injected, elements:', container.children.length);
  
  container.querySelector('button').addEventListener('click', () => {
    console.log('[Content] Button clicked');
  });
}

// Run when DOM is ready
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', injectUI);
} else {
  injectUI();
}

Debugging Message Passing

Content scripts communicate with the background via message passing:

// content.js - Send message with error handling
function sendToBackground(message) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage(message, (response) => {
      if (chrome.runtime.lastError) {
        console.error('[Content] Runtime error:', chrome.runtime.lastError.message);
        reject(chrome.runtime.lastError);
      } else {
        resolve(response);
      }
    });
  });
}

// Usage
sendToBackground({ type: 'GET_DATA', payload: { id: 123 } })
  .then(response => console.log('[Content] Response:', response))
  .catch(error => console.error('[Content] Failed:', error));

Screenshot: Console showing message passing between content script and background with timestamps.


Debugging Popup and Options Pages

Popups and options pages are simpler to debug—they’re essentially mini web pages.

Inspecting Popups

Two methods:

# Method 1: Right-click extension icon
Right-click extension icon → Inspect popup

# Method 2: From chrome://extensions
chrome://extensions → Find your extension → Inspect views → Popup

Important: Popup DevTools close when the popup closes. To persist:

  1. Pin the DevTools window
  2. Or use the Dock to bottom option to keep it visible

Debugging Popup Lifecycle

Popups have a short lifecycle—they open when clicked and close when the user clicks elsewhere:

// popup.js - Debug lifecycle
document.addEventListener('DOMContentLoaded', () => {
  console.log('[Popup] DOM loaded');
  
  // Initialize popup
  initializePopup();
  
  // Cleanup on unload
  window.addEventListener('unload', () => {
    console.log('[Popup] Closing - save state if needed');
  });
});

async function initializePopup() {
  // Load saved data
  const data = await chrome.storage.local.get('userPreferences');
  console.log('[Popup] Loaded preferences:', data);
  
  // Set up event listeners
  document.getElementById('save-btn').addEventListener('click', handleSave);
}

Debugging Options Pages

Options pages are full pages and easier to debug:

chrome://extensions → Find your extension → 
  "Extension options" link OR
  Inspect views → Options page

Screenshot: Options page DevTools showing storage inspection and event listeners.


Common Errors and Fixes

Error: “Could not establish connection”

Cause: Content script cannot reach background service worker.

Solution:

// Check connection before sending
chrome.runtime.sendMessage({ type: 'PING' }, (response) => {
  if (chrome.runtime.lastError?.message?.includes('Receiving end does not exist')) {
    console.error('Background worker not available');
    // Retry or show error to user
  }
});

Error: “Extension context invalidated”

Cause: Trying to use extension API after the extension was updated or reloaded.

Solution:

// Handle context invalidation
function safeStorageAccess() {
  try {
    return chrome.storage.local.get('key');
  } catch (error) {
    if (error.message.includes('Extension context invalidated')) {
      console.warn('Extension reloaded, retrying...');
      // Re-initialize or show reload prompt
    }
    throw error;
  }
}

Error: Service Worker Not Starting

Cause: Syntax errors in manifest.json or background script.

Solution:

  1. Check chrome://extensions/ for error badges
  2. Review manifest.json syntax
  3. Check Console for JavaScript errors
{
  "background": {
    "service_worker": "background.js",
    "type": "module"
  }
}

Error: Messages Not Received

Cause: Message listener not registered or incorrect message format.

Solution:

// Sender
chrome.runtime.sendMessage({ type: 'GREETING', name: 'Alice' });

// Receiver - must return true for async responses
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  console.log('Received:', message);
  
  if (message.type === 'GREETING') {
    sendResponse({ message: `Hello, ${message.name}!` });
    return true; // Required for async sendResponse
  }
});

Screenshot: chrome://extensions page showing an extension with a red error badge and expanded error details.


Logging Strategies

Effective logging is crucial for debugging extensions, especially in contexts that terminate (like service workers).

Structured Logging

// utils/logger.js - Structured logging utility
const LOG_LEVELS = {
  DEBUG: 0,
  INFO: 1,
  WARN: 2,
  ERROR: 3
};

const CURRENT_LEVEL = LOG_LEVELS.DEBUG; // Change for production

function log(level, ...args) {
  if (level >= CURRENT_LEVEL) {
    const timestamp = new Date().toISOString();
    const prefix = `[${timestamp}] [${level}]`;
    console.log(prefix, ...args);
  }
}

export const logger = {
  debug: (...args) => log('DEBUG', ...args),
  info: (...args) => log('INFO', ...args),
  warn: (...args) => log('WARN', ...args),
  error: (...args) => log('ERROR', ...args)
};

// Usage
logger.debug('Starting operation:', { userId: 123 });
logger.info('Operation completed');
logger.warn('Retrying...');
logger.error('Operation failed:', error);

Context-Aware Logging

// Include context in all logs
function getContext() {
  return {
    extensionId: chrome.runtime.id,
    url: window.location?.href,
    timestamp: Date.now()
  };
}

// Log with automatic context
function logWithContext(level, message, data = {}) {
  console[level]({
    ...getContext(),
    message,
    ...data
  });
}

logWithContext('info', 'User clicked button', { buttonId: 'save-btn' });

Persistent Logging for Service Workers

Service workers lose logs when they terminate. Use storage for persistence:

// background.js - Persistent logging
const LOG_KEY = 'extension_logs';
const MAX_LOGS = 100;

async function persistentLog(level, ...args) {
  const logs = await chrome.storage.local.get(LOG_KEY) || [];
  
  logs.push({
    timestamp: new Date().toISOString(),
    level,
    message: args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')
  });
  
  // Keep only recent logs
  if (logs.length > MAX_LOGS) {
    logs.splice(0, logs.length - MAX_LOGS);
  }
  
  await chrome.storage.local.set({ [LOG_KEY]: logs });
  
  // Also log to console
  console[level](...args);
}

// View logs in popup
chrome.storage.local.get(LOG_KEY, (result) => {
  console.log('All logs:', result[LOG_KEY]);
});

Remote Logging for Production

For tracking issues in production:

// background.js - Remote logging
async function logToServer(level, message, data) {
  if (!navigator.onLine) return; // Skip if offline
  
  try {
    await fetch('https://your-api.com/logs', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        extensionId: chrome.runtime.id,
        version: chrome.runtime.getManifest().version,
        level,
        message,
        data,
        timestamp: new Date().toISOString(),
        userAgent: navigator.userAgent
      })
    });
  } catch (error) {
    console.error('Failed to send log:', error);
  }
}

// Use for important errors only
logToServer('error', 'Feature X failed', { error: error.message, stack: error.stack });

Quick Debugging Checklist

When encountering issues, systematically check:



Summary

Debugging Chrome extensions requires understanding their unique architecture with multiple isolated contexts. Key takeaways:

  1. Use the right DevTools - Each context (service worker, popup, content script) requires different access methods
  2. Preserve logs - Enable “Preserve log” in console to see logs across service worker restarts
  3. Check context validity - Handle cases where extension context is invalidated
  4. Log strategically - Use structured, context-aware logging for better debugging
  5. Test all paths - Verify message passing, storage, and network requests in all contexts

With these techniques, you’ll be equipped to efficiently diagnose and fix any extension issue.


Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.

No previous article
No next article