Messaging
Message passing between extension components including content scripts, service workers, popups, and external pages
You are an expert in message passing between extension components for building browser extensions. ## Key Points - Always return `true` from `onMessage` listeners when calling `sendResponse` asynchronously; otherwise the message channel closes before the response is sent. - Define a clear message schema with `type` fields to route messages and avoid ambiguity when multiple components communicate. - Validate `sender.origin` or `sender.url` in `onMessageExternal` handlers to prevent unauthorized web pages from interacting with your extension. - Forgetting to return `true` in the `onMessage` listener for async responses — the port closes immediately and `sendResponse` silently fails.
skilldb get browser-extension-skills/MessagingFull skill: 275 linesMessaging — Browser Extension Development
You are an expert in message passing between extension components for building browser extensions.
Overview
Browser extensions are composed of multiple isolated contexts — content scripts, service workers, popups, options pages, and offscreen documents. These contexts communicate through Chrome's messaging APIs: one-time messages for request/response patterns and long-lived connections (ports) for ongoing communication channels.
Core Concepts
One-Time Messages
Simple request/response communication between any two extension contexts:
// SENDER — from popup, options page, or content script
const response = await chrome.runtime.sendMessage({
type: 'FETCH_DATA',
payload: { url: 'https://api.example.com/items' }
});
console.log('Received:', response);
// RECEIVER — in background service worker
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'FETCH_DATA') {
fetch(message.payload.url)
.then((res) => res.json())
.then((data) => sendResponse({ success: true, data }))
.catch((err) => sendResponse({ success: false, error: err.message }));
// Return true to indicate sendResponse will be called asynchronously
return true;
}
});
Messaging To and From Content Scripts
Communication between the background/popup and a specific tab's content script uses chrome.tabs.sendMessage:
// SEND to content script — from background or popup
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const response = await chrome.tabs.sendMessage(tab.id, {
type: 'EXTRACT_DATA',
selector: '.article-body'
});
// RECEIVE in content script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'EXTRACT_DATA') {
const el = document.querySelector(message.selector);
sendResponse({
text: el?.textContent || null,
html: el?.innerHTML || null
});
}
});
Long-Lived Connections (Ports)
For ongoing bidirectional communication:
// CONTENT SCRIPT — open a port
const port = chrome.runtime.connect({ name: 'content-stream' });
port.postMessage({ type: 'INIT', url: window.location.href });
port.onMessage.addListener((msg) => {
if (msg.type === 'HIGHLIGHT') {
highlightElements(msg.selector);
}
});
port.onDisconnect.addListener(() => {
console.log('Port disconnected');
});
// BACKGROUND — accept connections
chrome.runtime.onConnect.addListener((port) => {
if (port.name === 'content-stream') {
console.log('Content script connected from tab:', port.sender.tab.id);
port.onMessage.addListener((msg) => {
if (msg.type === 'INIT') {
port.postMessage({ type: 'HIGHLIGHT', selector: '.important' });
}
});
}
});
External Messaging
Allow web pages or other extensions to send messages to your extension:
{
"externally_connectable": {
"matches": ["https://www.example.com/*"],
"ids": ["other_extension_id"]
}
}
// FROM A WEB PAGE
chrome.runtime.sendMessage('YOUR_EXTENSION_ID', {
type: 'AUTH_TOKEN',
token: 'abc123'
}, (response) => {
console.log('Extension replied:', response);
});
// IN THE EXTENSION background.js
chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
// Validate the sender origin
if (sender.origin !== 'https://www.example.com') {
sendResponse({ error: 'Unauthorized' });
return;
}
if (message.type === 'AUTH_TOKEN') {
handleAuthToken(message.token);
sendResponse({ success: true });
}
});
Implementation Patterns
Typed Message Router
Organize message handling with a router pattern:
// message-router.js
class MessageRouter {
constructor() {
this.handlers = new Map();
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const handler = this.handlers.get(message.type);
if (!handler) {
console.warn(`No handler for message type: ${message.type}`);
return false;
}
const result = handler(message.payload, sender);
if (result instanceof Promise) {
result.then(sendResponse).catch((err) =>
sendResponse({ error: err.message })
);
return true; // async response
}
sendResponse(result);
return false;
});
}
on(type, handler) {
this.handlers.set(type, handler);
return this;
}
}
// Usage in background.js
const router = new MessageRouter();
router
.on('GET_SETTINGS', async () => {
return await chrome.storage.sync.get('settings');
})
.on('SAVE_SETTINGS', async (payload) => {
await chrome.storage.sync.set({ settings: payload });
return { saved: true };
})
.on('GET_TAB_INFO', async (payload, sender) => {
return { tabId: sender.tab?.id, url: sender.tab?.url };
});
Content Script Communication Bridge
For communication between the page's JS context and the extension:
// content.js — bridge between page world and extension
window.addEventListener('message', (event) => {
if (event.source !== window) return;
if (event.data?.source !== 'my-ext-page') return;
// Forward to background
chrome.runtime.sendMessage({
type: 'FROM_PAGE',
payload: event.data.payload
}).then((response) => {
// Send response back to page
window.postMessage({
source: 'my-ext-content',
payload: response
}, '*');
});
});
// In a MAIN world script or injected <script>
window.postMessage({
source: 'my-ext-page',
payload: { action: 'getData', key: 'user' }
}, '*');
window.addEventListener('message', (event) => {
if (event.data?.source === 'my-ext-content') {
console.log('Response from extension:', event.data.payload);
}
});
Broadcasting to All Content Scripts
async function broadcastToContentScripts(message) {
const tabs = await chrome.tabs.query({});
const results = await Promise.allSettled(
tabs.map((tab) =>
chrome.tabs.sendMessage(tab.id, message).catch(() => null)
)
);
return results;
}
// Usage
broadcastToContentScripts({ type: 'SETTINGS_CHANGED', settings: newSettings });
Best Practices
- Always return
truefromonMessagelisteners when callingsendResponseasynchronously; otherwise the message channel closes before the response is sent. - Define a clear message schema with
typefields to route messages and avoid ambiguity when multiple components communicate. - Validate
sender.originorsender.urlinonMessageExternalhandlers to prevent unauthorized web pages from interacting with your extension.
Core Philosophy
Messaging in a browser extension is inter-process communication, and it should be treated with the same rigor as a network protocol. Define a clear message schema with typed type fields, document the expected payloads, and validate inputs on the receiving end. Unstructured messaging — sending arbitrary objects without a routing mechanism — quickly becomes unmaintainable as the extension grows.
Prefer the invoke/handle pattern (sendMessage with response) over fire-and-forget (sendMessage without callback) for any operation where the sender needs to know the outcome. Fire-and-forget is appropriate for logging and analytics, but for data fetching, state changes, or user-facing actions, the sender should always know whether the operation succeeded. This mirrors the request/response pattern of HTTP and makes error handling explicit.
Every message crosses a trust boundary. Content scripts run in the context of arbitrary web pages, and their messages could be spoofed or manipulated by malicious page scripts (especially if the page can access the content script's DOM). Always validate the structure and content of messages in the background service worker before acting on them. Treat content script messages the same way you would treat user input on a server.
Anti-Patterns
-
Using a single generic message type for everything — sending
{ action: "do-stuff", data: ... }without a structured type system makes it impossible to route messages cleanly or add TypeScript type safety; define distinct message types. -
Forgetting
return truefor async responses — whensendResponseis called inside an async callback, the message channel closes before the response is sent unless the listener explicitly returnstrue. -
Exposing raw
chrome.runtime.sendMessageto page scripts — allowing the page's JavaScript to trigger extension actions without validation creates a security hole; always validate sender origin and message structure. -
Broadcasting to all tabs when only one is relevant — iterating over all tabs and sending messages to each when only the active tab needs the data wastes resources and can trigger errors on tabs where the content script is not injected.
-
Nesting message handlers deeply — handling dozens of message types in a single
onMessagelistener with nested if/else chains is unreadable; use a router pattern or handler map to dispatch by message type.
Common Pitfalls
- Forgetting to return
truein theonMessagelistener for async responses — the port closes immediately andsendResponsesilently fails. - Sending a message to a content script in a tab where the content script has not been injected — this throws an error; always handle the case where no receiver exists by wrapping in try/catch or checking tab status first.
Install this skill directly: skilldb add browser-extension-skills
Related Skills
Background Workers
Service workers and background scripts for event-driven extension logic, lifecycle management, and persistent tasks
Content Scripts
Content scripts for DOM manipulation, page interaction, and isolated world execution in browser extensions
Cross-Browser
Cross-browser extension compatibility across Chrome, Firefox, and Safari with API normalization and build strategies
Extension Publishing
Publishing browser extensions to Chrome Web Store and Firefox Add-ons including review processes and automation
Manifest V3
Chrome Manifest V3 fundamentals including configuration, permissions, and migration from V2
Popup UI
Building popup and options page UIs for browser extensions with responsive layouts and framework integration