PSA: Messaging API Changes for Chromium Extension Developers

8 views
Skip to first unread message

Justin Lulejian

unread,
6:04 PM (5 hours ago) 6:04 PM
to Chromium Extensions, Oliver Dunk

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


// background.js

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


Reply all
Reply to author
Forward
0 new messages