Chrome Extension Message Passing — Developer Guide
9 min readMessage Passing Best Practices
Overview
Effective communication between extension components is critical for building robust Chrome extensions. This guide covers the recommended patterns for message passing, common pitfalls to avoid, and how to build type-safe, reliable messaging systems in your extension.
Chrome Extension Message Flow Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ CHROME EXTENSION MESSAGE PASSING FLOW │
└─────────────────────────────────────────────────────────────────────────────┘
CONTENT SCRIPT BACKGROUND SERVICE WORKER POPUP / OPTIONS
(injected in page) (central hub) (UI extension pages)
───────────────── ─────────────────────── ───────────────────
│ │ │
│ chrome.runtime.sendMessage() │ │
├───────────────────────────────────►│ │
│ { type: 'GET_DATA' } │ │
│ │ │
│ │ ┌─────────────────────────────┐ │
│ │ │ Message Router/Handler │ │
│ │ │ - Validates message │ │
│ │ │ - Routes to appropriate │ │
│ │ │ handler │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ ◄────────────────────────────────┤ │
│ { data: {...} } response │ │
│ │ │
│ │ chrome.runtime.sendMessage() │
│ ├───────────────────────────────────► │
│ │ { type: 'UPDATE_UI' } │
│ │ │
│ │ ◄────────────────────────────────
│ │ { success: true }
│
│
────────────── ─────────────────────── ───────────────────
Page Context Background Context Extension Context
(isolated world) (service worker) (privileged APIs)
┌─────────────────────────────────────────────────────────────────────────────┐
│ CONNECTION PORTS (chrome.runtime.connect) │
└─────────────────────────────────────────────────────────────────────────────┘
TAB 1 ─────────────┐
(content script) │ chrome.tabs.connect(tabId, { name: 'stream' })
├─────────────────────────┐
TAB 2 ─────────────┤ │
(content script) │ ▼
│ ┌─────────────────────┐
POPUP ──────────────┼──────────────►│ BACKGROUND SW │◄── Persistent
│ │ Port Manager │ Connection
│ └─────────────────────┘ (auto-reconnect)
│ │
TAB 3 ─────────────┘ │
(content script) ▼
┌─────────────────────┐
│ Message Stream │
│ Bi-directional │
└─────────────────────┘

## Choose the Right Method
## Choose the Right Method {#choose-the-right-method}
Chrome provides several messaging APIs, each suited for different use cases:
- **One-time messages**: Use `chrome.runtime.sendMessage` for simple request-response patterns between the background service worker and content scripts or popup.
- **Targeted to tab**: Use `chrome.tabs.sendMessage` when you need to send a message specifically to a content script running in a particular tab.
- **Persistent connection**: Use `chrome.runtime.connect` when you need streaming or frequent messages between components. Ports maintain an open channel and handle reconnection automatically.
- **Cross-extension**: Use `runtime.sendMessage` with the `extensionId` parameter to communicate with other extensions.
## Message Structure {#message-structure}
Always structure your messages consistently for maintainability and type safety:
```js
// Good: Consistent message structure
{ type: 'GET_DATA', payload: { userId: 123 } }
{ type: 'NOTIFY', payload: { message: 'Done!' } }
Define all message types in shared constants to avoid typos and enable tooling:
// messages.js - shared constants
export const MessageTypes = {
GET_DATA: 'GET_DATA',
SET_DATA: 'SET_DATA',
NOTIFY: 'NOTIFY',
FETCH_STATUS: 'FETCH_STATUS'
};
For TypeScript projects, consider using @theluckystrike/webext-messaging which provides typed wrappers and reduces boilerplate.
Common Pitfalls
Unchecked lastError
Always check chrome.runtime.lastError in callbacks. This is a common source of silent failures:
// Bad: Ignoring lastError
chrome.runtime.sendMessage({ type: 'PING' }, (response) => {
console.log('Response:', response); // May be undefined!
});
// Good: Checking lastError
chrome.runtime.sendMessage({ type: 'PING' }, (response) => {
if (chrome.runtime.lastError) {
console.error('Messaging error:', chrome.runtime.lastError.message);
return;
}
console.log('Response:', response);
});
For promise-based calls, catch rejected promises to handle errors properly. The common error “Could not establish connection. Receiving end does not exist.” indicates the content script isn’t loaded.
Missing return true
The onMessage listener MUST return true if you intend to send an asynchronous response:
// Bad: sendResponse won't work for async operations
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'FETCH_DATA') {
fetchData().then(sendResponse); // Too late! Channel closed
}
});
// Good: Return true to keep channel open
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'FETCH_DATA') {
fetchData().then(sendResponse);
return true; // Keep message channel open for async response
}
});
// Modern MV3: Return a Promise instead
chrome.runtime.onMessage.addListener((msg, sender) => {
if (msg.type === 'FETCH_DATA') {
return fetchData(); // Promise automatically keeps channel open
}
});
Dead Listeners
Content script listeners die when the page navigates. This is especially problematic for SPAs:
- Re-inject content scripts when needed using
chrome.scripting.executeScript - Check if a content script exists before sending messages using a ping-pong pattern
- Use
chrome.runtime.onConnectfor automatic reconnection handling
Async Response Pattern
Here’s a complete example of the recommended async response pattern:
// Background script
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'FETCH_USER') {
// Return promise - MV3 handles async automatically
return fetchUserData(msg.payload.userId);
}
if (msg.type === 'GET_TAB_DATA') {
const promise = getTabData(sender.tab.id);
return promise;
}
});
async function getTabData(tabId) {
try {
const response = await chrome.tabs.sendMessage(tabId, { type: 'PING' });
return { success: true, data: response };
} catch (error) {
return { success: false, error: error.message };
}
}
Error Handling
Implement robust error handling in your messaging layer:
- Wrap
sendMessagecalls in try-catch blocks - Add manual timeouts for responses to prevent hanging
- Implement retry logic with exponential backoff
- Log messaging errors for debugging and monitoring
function sendMessageWithTimeout(message, timeout = 5000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Message timeout'));
}, timeout);
chrome.runtime.sendMessage(message, (response) => {
clearTimeout(timer);
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(response);
}
});
});
}
Performance
Keep your messaging performant:
- Keep messages small: Avoid sending large objects or serializing DOM elements
- Use ports for frequent messages: Connections have less overhead than repeated message calls
- Batch updates: Instead of sending a message per item, collect changes and send bulk updates
- Consider structured clone: Be aware of what can be passed through the messaging system
Code Examples
Type-Safe Message Handler with Router
// message-router.js
const handlers = {
[MessageTypes.GET_DATA]: handleGetData,
[MessageTypes.SET_DATA]: handleSetData,
[MessageTypes.NOTIFY]: handleNotify
};
chrome.runtime.onMessage.addListener((msg, sender) => {
const handler = handlers[msg.type];
if (!handler) {
console.warn(`No handler for message type: ${msg.type}`);
return false;
}
return handler(msg.payload, sender);
});
Error-Resilient SendMessage Wrapper
// messaging-utils.js
export async function sendMessageSafe(message) {
try {
return await new Promise((resolve, reject) => {
chrome.runtime.sendMessage(message, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(response);
}
});
});
} catch (error) {
console.error('Message send failed:', error);
return null;
}
}
Port-Based Streaming Pattern
// background.js - Create port
const port = chrome.tabs.connect(tabId, { name: 'stream' });
port.postMessage({ type: 'START_STREAM' });
port.onMessage.addListener((msg) => {
console.log('Stream update:', msg);
});
// content.js - Listen on port
chrome.runtime.onConnect.addListener((port) => {
if (port.name === 'stream') {
port.onMessage.addListener((msg) => {
// Handle streaming messages
});
}
});
Message Timeout Utility
// with-timeout.js
export function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
)
]);
}
// Usage
const response = await withTimeout(
chrome.runtime.sendMessage({ type: 'FETCH_DATA' }),
3000
);
Cross-References
Related Articles
Related Articles
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.