chrome.runtime.onMessage response with async await

Viewed 16761

I want to use async await in an onMessage listener:

chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) =>{
    var key = await getKey();
    sendResponse(key);
});

However I get undefined when I send a message.

From the documentation for chrome.runtime.onMessage.addListener:

This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until sendResponse is called).

This works when I use a callback.

chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) =>{
    getKey(key => {
        sendResponse(key);
    });
    return true;
});

However I would like to leverage the await syntax. But it does not seem to work and still returns undefined:

chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) =>{
    var key = await getKey();
    sendResponse(key);
    return true;
});
7 Answers

Option #1 - Simplest

Extract to an async function.

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  doSomethingWith(request).then(sendResponse);
  return true; // return true to indicate you want to send a response asynchronously
});

async function doSomethingWith(request) {
  var key = await getKey();
  // await .....
  return key;
}

The return value of an async function is implicitly wrapped in Promise.resolve. See async doc.

The return true; statement did the trick. It tells Chrome that you want to send a response asynchronously.

See onMessage.

Option #2 - Utility

If you feel that you will use it most frequently, create a utility, something like:

const wrapAsyncFunction = (listener) => (request, sender, sendResponse) => {
  // the listener(...) might return a non-promise result (not an async function), so we wrap it with Promise.resolve()
  Promise.resolve(listener(request, sender)).then(sendResponse);
  return true; // return true to indicate you want to send a response asynchronously
};

chrome.runtime.onMessage.addListener(
  wrapAsyncFunction(async (request, sender) => {
    console.log(request, sender);

    const key = await getKey();
    // await .....
    return key;
  })
);

Option #3 - More generic

Use mozilla/webextension-polyfill if you prefer to "cross browsers" extension.

Example:

var browser = require("webextension-polyfill");

browser.runtime.onMessage.addListener(async (msg, sender) => {
  console.log("BG page received message", msg, "from", sender);
  console.log("Stored data", await browser.storage.local.get());
});

browser.browserAction.onClicked.addListener(() => {
  browser.tabs.executeScript({file: "content.js"});
});

Not sure if Chrome extension runtime environment supports the async/await syntax, but you can use a transpiler (i.e. Babel) to have it converted to for example ES5. Then you can define a wrapper function like this:

function asChromeListener(listener) {
  return (message, sender, sendResponse) => {
    const returnValue = listener(message, sender);

    if (isPromise(returnValue)) {
      returnValue.then(sendResponse);
      return true;
    }
    else {
      if (typeof returnValue !== 'undefined') {
        sendResponse(returnValue);
      }
      return false;
    }
  };
}

function isPromise(value) {
  return typeof value === 'object' && value !== null && 'then' in value && 'catch' in value;
}

Which you can then use like:

chrome.runtime.onMessage.addListener(asChromeListener(async (message, sender) => {
  return await doMyAsyncWork(message);
});

Since we use TypeScript, here's also the snippet that we actually use (with generic types).

export function asChromeListener<M, S, R extends Function>(listener: (message: M, sender: S) => any) {
  return (message: M, sender: S, sendResponse: R) => {
    const returnValue = listener(message, sender);

    if (isPromise(returnValue)) {
      returnValue.then(sendResponse);
      return true;
    }
    else {
      if (typeof returnValue !== 'undefined') {
        sendResponse(returnValue);
      }
      return false;
    }
  };
}

function isPromise(value: any) {
  return typeof value === 'object' && value !== null && 'then' in value && 'catch' in value;
}

Function to call (at most once) when you have a response. The argument should be any JSON-ifiable object. If you have more than one onMessage listener in the same document, then only one may send a response. This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until sendResponse is called).

https://developer.chrome.com/docs/extensions/reference/runtime/#event-onMessage

have to say the official doc is really hard to read.

The closest I could get:

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    (async () => {
        var key = await getKey();
        sendResponse(key);
    })();
    return true;
});

I was unable to use async/await, however I did manage to make it work using then. You need to return true at the very end of your onMessage function.

Here is an example of how I'm asynchronously retrieving the current active tab to my content script.

background.js

chrome.runtime.onMessage.addListener(function (data, sender, sendResponse) {

  if (data.type === 'getTab') {
    const promise = new Promise(async (resolve) => {
      /* gets active tab info */
      let queryOptions = { active: true, currentWindow: true };
      let [tab] = await chrome.tabs.query(queryOptions);

      resolve(tab);
    });

    promise.then((tab) => {
      sendResponse(tab);
    });
  }


  return true;
});

content.js

        chrome.runtime.sendMessage(
          {
            type: 'getTab'
          },
          function (data) {

            if (data.type === 'getTab') {
              console.log('my tab', data);
            }
          }
        );

I hope this helps someone out there. I've spent more time than I'd care to admit on this one.

you could try this

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  new Promise(async send => {
     

    var key = await getKey();
    send(key);


  }).then(sendResponse)
  return true;
});
Related