I'm building addon for firefox user can take screenshot, but browser.runtime.sendMessage returns undefined

I’m trying to implement a function that users can use to take screenshots using browser.tabs.captureVisibleTab .

But browser.runtime.sendMessage immediately returns undefined.

In console.log("dataUrl ::: ", dataUrl) of browser.runtime.onMessage.addListener it logged data url, so it’s working. But somehow, it’s undefined in screenshot.addEventListener My current code is as below: It works fine in Chrome (I changed browsers with Chrome, of course).

I appreciate any advice.

// manifest.json

"permissions": ["activeTab", "tabs", "storage", "webRequest", "<all_urls>"],
  "host_permissions": ["<all_urls>"]

// popup.js

screenshot.addEventListener("click", async () => {
  try {
    const response = await browser.runtime.sendMessage({
      type: "takeScreenshot",
    });
    console.log("response is ", response); // undefined
    if (response.dataUrl) {
      const img = document.createElement("img");
      img.src = response.dataUrl;
      document.body.appendChild(img);
    }
  } catch (error) {
    console.error("Error sending message:", error);
  }
});

// background.js

browser.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
  
  if (message.type === "takeScreenshot") {
    const option = { active: true, currentWindow: true }; 
    await browser.tabs.query(option, async (tabs) => {
      const activeTab = tabs[0];
      if (!activeTab) {
        await sendResponse({ error: "No active tab." });
        return;
      }
      await browser.tabs.captureVisibleTab(
        activeTab.windowId,
        { format: "png" },
       
        async (dataUrl) => {
          if (browser.runtime.lastError) {
            await sendResponse({ error: browser.runtime.lastError.message });

            return;
          }
          console.log("dataUrl ::: ", dataUrl); //it logs correct dataUrl

          await sendResponse({ dataUrl: dataUrl }); 
        }
       
      );
    });
    return true;
  }
});

I enabled the “<all_urls>” permission in the Add-ons Manager according to this . But it still returns the same result.

This looks like the source of the problem - the “async”. Function marked as “async” will ALWAYS return a Promise. And that’s a problem in this case, because this specific message handler in Firefox supports “returning a value by returning a Promise”.

More info:

  1. It’s recommended to NOT use async callback in this specific handler.
  2. it’s also recommended to NOT use “sendResponse” :slight_smile:, but instead return a Promise when you want to send a response.
    Note however that "Respond with Promise" is not supported by Chrome, but seeing how you use “browser” namespace, I would guess you are already using webextension-polyfill which adds this feature to Chrome.
2 Likes

Thank you for your reply. I tried responding with promise like

browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === "takeScreenshot") {
    const option = { active: true, currentWindow: true }; 
    browser.tabs.query(option, (tabs) => {
      const activeTab = tabs[0];
      if (!activeTab) {
     
        return { error: "No active tab." };
      }
      browser.tabs.captureVisibleTab(
        activeTab.windowId,
        { format: "png" },
 
        (dataUrl) => {
          if (browser.runtime.lastError) {
           
            return { error: browser.runtime.lastError.message };
          }
          console.log("dataUrl ::: ", dataUrl);
          return new Promise((resolve, reject) => {
            resolve({ dataUrl });
          });
         
        }

      );
    });
   
  }

});

But it didn’t work. Instead, I moved browser.tabs.captureVisibleTab to popup.js and now it works!

I’m glad you found a better way.
Let me just give you a few more tips for the future.

  1. in the browser.runtime.onMessage handler, it’s best to keep as little code as possible. So after the
    if (message.type === "takeScreenshot") {
    you should not write the whole code handling the async logic, but instead put a function call, for example:
    if (message.type === "takeScreenshot") { return takeScreenshot() }
    See? Now the “takeScreenshot” function can be made async and the message handler will return a promise only when needed.

  2. if you want to wrap a value to a promise, don’t use new Promise((resolve, reject) => {..., instead use simple Promise.resolve({dataUrl}).

3 Likes