Dear Chromium Extension Developers,
We're excited to announce several related upcoming changes in Chrome 144. These are currently available in the latest Canary. The change should reach Stable on January 13th, 2026.
TL;DR
Launching the browser namespace by default, as an optional alternative to chrome
Supporting Promise returns in runtime.onMessage listeners
Changes to the messaging APIs, designed to better align with webextension-polyfill (polyfill)
These improvements aim to make extension development more consistent and predictable across different browsers. They also enable the browser namespace we announced earlier, which will launch with these changes.
We strongly encourage you to test these new features to see if your extension(s) require any messaging adjustments since the polyfill will become a noop when these changes take effect.
Supporting Promise returns from runtime.OnMessage listeners
We are implementing support for returning promises directly from runtime.OnMessage listeners. With this and promise support now available for other APIs, we hope most extension developers will no longer need the polyfill.
// content_script.js
let response = await chrome.runtime.sendMessage('test');
console.assert(response === 'promise resolved');
// background.js
function onMessageListener(message, sender, sendResponse) {
return new Promise((resolve) => {
resolve('promise resolved');
}
}
chrome.runtime.onMessage.addListener(onMessageListener);
runtime.OnMessage Behavior Alignment with Polyfill
In addition to promise support, we are making adjustments to how runtime.OnMessage() behaves with error scenarios to more closely match the polyfill.
Listeners that throw errors before responding
The first listener to throw an error will cause the sender’s promise to reject.
// content_script.js
chrome.runtime.sendMessage('test').reject((reject) => {
console.assert(reject.message === 'error!');
});
// background.js
function onMessageListener(message, sender, sendResponse) {
throw new Error('error!');
}
chrome.runtime.onMessage.addListener(onMessageListener);
function onMessageListener(message, sender, sendResponse) {
sendResponse('response');
}
chrome.runtime.onMessage.addListener(onMessageListener);
Listener ordering still matters:
// content_script.js
let response = await chrome.runtime.sendMessage('test');
console.assert(response === 'response');
// background.js
function onMessageListener(message, sender, sendResponse) {
sendResponse('response');
}
chrome.runtime.onMessage.addListener(onMessageListener);
function onMessageListener(message, sender, sendResponse) {
throw new Error('error!');
}
chrome.runtime.onMessage.addListener(onMessageListener);
We’ve also made improvements to handling unserializable responses. See the next section for more details as it is a slightly different implementation.
Subtle differences from the polyfill and runtime.onMessage (MDN)
While we aim to closely match the polyfill and runtime.onMessage on MDN, there will still be some subtle differences. This section outlines all known differences to help you smoothly transition away from using the polyfill if it's no longer needed. If you find any significant additional ones please let us know!
Returned promises that reject with anything other than an Error-type (undefined, etc.)
They’ll receive a generic static error message, rather than the .message property from the thrown object which the polyfill would return.
// content_script.js
// Chromium
chrome.runtime.sendMessage('test').reject((reject_reason) => {
console.assert(
reject.message === 'A runtime.onMessage listener\'s promise rejected without an ' +
'Error');
});
// background.js
function onMessageListener(message, sender, sendResponse) {
return new Promise((unusedResolve, reject) => {
reject(undefined);
});
}
chrome.runtime.onMessage.addListener(onMessageListener);
Listeners that respond with unserializable responses
This now rejects the sender’s promise like the polyfill does, but it does it immediately and with a specific error rather than waiting until the worker stops.
// content_script.js
chrome.runtime.sendMessage('test').reject((reject_reason) => {
console.assert(reject.message === 'Error: Could not serialize message.!');
});
// background.js
function onMessageListener(message, sender, sendResponse) {
// Functions aren't currently serializable but we're working on it.
sendResponse(() => {}); // a TypeError is thrown here
}
chrome.runtime.onMessage.addListener(onMessageListener);
async functions passed to runtime.OnMessage
async functions always return promises, so the browser treats them as if a promise was returned from the listener, even if it never responds or calls sendResponse. This is similar to how runtime.onMessage() as described on MDN handles async functions as sending an asynchronous response using a Promise. However, our response return values differ slightly:
// content_script.js
let response = await chrome.runtime.sendMessage('test');
console.assert(response === true); // MDN
console.assert(response === null); // Chromium
// background.js
async function onMessageListener(message, sender) {
// Do some things, but without sendResponse
}
chrome.runtime.onMessage.addListener(onMessageListener);
multiple async functions passed to runtime.OnMessage
runtime.onMessage() MDN documentation describes that passing an async function to a listener will prevent any other listeners (async or not) from responding to the sender’s message. Chromium does not have this restriction. Multiple async or non-async functions can be added as runtime.onMessage() listeners. The first listener to respond (synchronously or asynchronously) will have its response sent back to the message sender.
// content_script.js
let response = await chrome.runtime.sendMessage('test');
console.assert(response === 'slower response'); // MDN
console.assert(response === 'faster response'); // Chromium
async function onMessageSlowerRespondingListener() {
return new Promise((resolve) => {
setTimeout(resolve, 1000, 'slower response');
});
}
chrome.runtime.onMessage.addListener(slowerAsyncFunctionResponse);
async function onMessageFasterRespondingListener(message, sender) {
return 'faster response';
}
chrome.runtime.onMessage.addListener(onMessageListener);
We hope that this change will make it easier to develop extensions across multiple browsers.
If you have any questions, please feel free to reach out to us.
Thank you,
Justin, on behalf of the Chromium Extensions Team
> // content_script.js
> chrome.runtime.sendMessage('test').reject((reject) => {
> console.assert(reject.message === 'error!');
> });
> chrome.runtime.sendMessage('test').reject((reject_reason) => {
> console.assert(reject.message === 'Error: Could not serialize message.!');
> });
--
You received this message because you are subscribed to the Google Groups "Chromium Extensions" group.
To unsubscribe from this group and stop receiving emails from it, send an email to chromium-extens...@chromium.org.
To view this discussion visit https://groups.google.com/a/chromium.org/d/msgid/chromium-extensions/506b499d-c278-4408-9955-3cab48c73352n%40chromium.org.
Hi all,
Thank you for all the observations and feedback; it's been very helpful!
@PhistucK: Yes, that was my mistake. Thank you for .catch()-ing that!
@Juraj + Alexander + DreamBuilder Team:
Multiple `async` function listeners (as well as synchronous and other asynchronous responses) are supported and they should race to respond to the sender. I responded to Alexander's bug report, which I believe explains the issue and why you're seeing this behavior. The key thing to keep in mind is that `async` functions always return a `Promise`, even if one doesn't explicitly return a value (which implicitly resolves the Promise to undefined).
AFAICT we're aligned with the Firefox behavior (minus the undefined -> null conversion that happens), so this is working as intended. I'll be documenting this with a specific example in the messaging docs to make this clearer for other developers. However, if after taking a look at my response in the bug report it seems like I'm misunderstanding the issue, please let me know!
Max is correct that the undefined -> null conversion is related to Chromium's JSON.stringify serialization of the messages. I'm currently working on finishing up the structured cloning implementation. Once that's done, `undefined` will be sent back to the message sender unmodified. It should also support Blobs, so I'm glad to hear that will be helpful.