Debugging Chrome Extensions: Tips, Tools, and Techniques
15 min readDebugging 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
- Use Chrome DevTools for extension debugging
- Inspect and debug background service workers
- Debug content scripts in isolated contexts
- Debug popup and options pages
- Identify and fix common extension errors
- Implement effective logging strategies
Setting Up Your Debugging Environment
Before debugging, configure Chrome for extension development:
- Open
chrome://extensions/in Chrome - Enable Developer mode using the toggle in the top-right corner
- 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
- Console: View logs and errors from all extension contexts
- Sources: Set breakpoints in extension code
- Network: Monitor network requests (note: some requests may not appear)
- Application: Check storage, service worker registration, and manifest
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:
- Status indicator: Shows “Activated” and “Running” when healthy
- Inspect links: For multiple workers, click the appropriate one
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:
- In DevTools Console tab, click ⚙️ (Settings)
- 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
- Open DevTools (F12) on a page where your content script runs
- In the debugger dropdown (top-left), find your extension name
- 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:
- Pin the DevTools window
- 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:
- Check
chrome://extensions/for error badges - Review manifest.json syntax
- 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:
- Manifest version (should be 3) and required fields
- All required permissions declared in manifest.json
- Service worker status (should show “Activated and Running”)
- Console errors in all extension contexts
- Message passing listener returns
truefor async responses - Content script matches specified in
content_scriptspatterns - CSP (Content Security Policy) not blocking your code
- Storage quota not exceeded
Related Articles
- Service Worker Debugging — Deep dive into debugging background service workers
- Common Errors and Fixes — Reference guide for frequent extension errors
- Extension Logging Patterns — Best practices for logging in extensions
Summary
Debugging Chrome extensions requires understanding their unique architecture with multiple isolated contexts. Key takeaways:
- Use the right DevTools - Each context (service worker, popup, content script) requires different access methods
- Preserve logs - Enable “Preserve log” in console to see logs across service worker restarts
- Check context validity - Handle cases where extension context is invalidated
- Log strategically - Use structured, context-aware logging for better debugging
- 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.