Using runtime messages
Different parts of a WebExtension can communicate using runtime messages. The receiving end must register a listener function:
function listener(data, sender, sendResponse) {
// Do something with the received data and sender parameters.
switch (data.msg) {
case "greet":
console.log(`Hello from tab ${sender.tab.id}`);
sendResponse("Hi there!");
break;
case "farewell":
console.log(`Goodbye from tab ${sender.tab.id}`);
sendResponse("Goodbye!");
break;
}
}
messenger.runtime.onMessage.addListener(listener);
Any other part of the extension can now send messages to this listener:
await messenger.runtime.sendMessage({ msg: "greet" });
There are a few quirks that developers should be aware of:
A script cannot send a message to itself. A message sent from the background will not be received by listeners in the background script.
The listener function should not be asynchronous. If multiple
runtime.onMessagelisteners are registered, asynchronous listeners can trigger unexpected behavior. Even with only one listener, it is best practice to keep them synchronous to avoid future debugging issues.When multiple listeners are registered for the
runtime.onMessageevent, each receives the message. How they respond determines which result is returned to the sender.
Using multiple onMessage listeners with synchronous responses
If a WebExtension registers multiple runtime.onMessage listeners (in the same file or in different files), all listeners will be called when a message is sent. The simplest way to define which listener should respond is by using the sendResponse() function selectively. For example:
function listener(data, sender, sendResponse) {
switch (data.msg) {
case "greet":
console.log(`Hello from tab ${sender.tab.id}`);
sendResponse("Hi there!");
break;
}
}
messenger.runtime.onMessage.addListener(listener);
function listener(data, sender, sendResponse) {
switch (data.msg) {
case "farewell":
console.log(`Goodbye from tab ${sender.tab.id}`);
sendResponse("Goodbye!");
break;
}
}
messenger.runtime.onMessage.addListener(listener);
// Send a "greet" message
let response = await messenger.runtime.sendMessage({ msg: "greet" });
console.log(response); // "Hi there!"
If both popup files are loaded, both listeners will be invoked, but only the listener in popup1.js sends a response.
Using multiple onMessage listeners with asynchronous responses
If a WebExtension registers multiple runtime.onMessage listeners, and at least one needs to perform asynchronous operations before responding, the situation becomes more complex. The following two approaches for handling asynchronous responses are supported:
The listener returns
trueto indicate that it will respond asynchronously. ThesendResponse()channel will be kept open until the listener uses it to send its response.popup1.jsasync function performAsyncOperation(data, sender) { // Simulate async work. await new Promise(resolve => setTimeout(resolve, 1000)); return "Hi there!"; } function listener(data, sender, sendResponse) { switch (data.msg) { case "greet": console.log(`Hello from tab ${sender.tab.id}`); performAsyncOperation(data, sender).then( // Send the response when the async operation is complete. response => sendResponse(response) ); break; } // Indicate that we will respond asynchronously, keeping the sendResponse channel open. return true; } messenger.runtime.onMessage.addListener(listener);
popup2.jsfunction listener(data, sender, sendResponse) { switch (data.msg) { case "farewell": console.log(`Goodbye from tab ${sender.tab.id}`); sendResponse("Goodbye!"); break; } } messenger.runtime.onMessage.addListener(listener);
Both listeners will be called whenever a message is sent. If the message is “greet”, the listener in
popup1.jsperforms its asynchronous operation and responds after a delay. If the message is “farewell”, the listener inpopup2.jsresponds immediately.Note
The return value of
messenger.runtime.sendMessage()received by the sending party is always aPromise, regardless of whether the message is handled synchronously or asynchronously.The listener returns a
Promisefor the response. This approach is more straightforward and easier to read. For example:popup1.jsasync function performAsyncOperation(data, sender) { // Simulate async work. await new Promise(resolve => setTimeout(resolve, 1000)); return "Hi there!"; } function listener(data, sender, sendResponse) { switch (data.msg) { case "greet": console.log(`Hello from tab ${sender.tab.id}`); // Return a Promise that resolves to the response. return performAsyncOperation(data, sender); } } messenger.runtime.onMessage.addListener(listener);
popup2.jsfunction listener(data, sender, sendResponse) { switch (data.msg) { case "farewell": console.log(`Goodbye from tab ${sender.tab.id}`); // Return a resolved Promise for the response. return Promise.resolve("Goodbye!"); } } messenger.runtime.onMessage.addListener(listener);
Both listeners will be called whenever a message is sent. If the message is “greet”, the listener in
popup1.jsperforms its asynchronous operation and returns a Promise for its response. If the message is “farewell”, the listener inpopup2.jsresponds with an already resolved Promise.Note
The implementation in
popup2.jscould also simply usesendResponse("Goodbye!")instead of returning a resolved Promise. Both approaches are valid and will work correctly.The important point to remember is that listeners should return a
Promiseonly when they actually handle the message. Consider the following alternative implementation using an asynchronous listener function forpopup1.js:popup1.jsasync function listener(data, sender, sendResponse) { switch (data.msg) { case "greet": console.log(`Hello from tab ${sender.tab.id}`); // Simulate async work await new Promise(resolve => setTimeout(resolve, 1000)); return "Hi there!"; } } messenger.runtime.onMessage.addListener(listener);
This may cause the sender to receive an
undefinedresponse when sending a “farewell” message, since the listener inpopup1.jsalways returns aPromise, even when it doesn’t handle the message. Depending on the registration order and other timing effects, the correct response frompopup2.jsmay be ignored.