Dev.to WebDev 🛠 Dev 👁 0 📖 1 min read

Two MV3 errors you hit right after bumping manifest_version (and the fix)

You change "manifest_version": 2 to 3 and the extension breaks. Two errors show up a lot: Service worker registration failed chrome.browserAction is undefined Below is a small made-up teaching example (a tab counter)

You change "manifest_version": 2 to 3 and the extension breaks. Two errors show up a lot:

  • Service worker registration failed
  • chrome.browserAction is undefined

Below is a small made-up teaching example (a tab counter), not a real project or client case. It is a partial migration, checked by reading the files only; it was not run in a browser.

1. The background page has to become a service worker

MV2 (before):

"background": { "scripts": ["background.js"], "persistent": true },
"browser_action": { "default_title": "Tab count" }

MV3 (after):

"background": { "service_worker": "background.js" },
"action": { "default_title": "Tab count" }

MV3 has no persistent background pages. browser_action was renamed action, and chrome.browserAction became chrome.action in code.

2. Don't keep state in a global variable

Before:

let count = 0; // in-memory state: lost when an MV3 worker stops
function refresh() {
  chrome.tabs.query({}, (tabs) => {
    count = tabs.length;
    chrome.browserAction.setBadgeText({ text: String(count) });
  });
}
chrome.tabs.onCreated.addListener(refresh);
chrome.tabs.onRemoved.addListener(refresh);
refresh();

The service worker is stopped when idle, so count is lost. After:

async function refresh() {
  const tabs = await chrome.tabs.query({});
  await chrome.action.setBadgeText({ text: String(tabs.length) });
}
chrome.tabs.onCreated.addListener(refresh);
chrome.tabs.onRemoved.addListener(refresh);
chrome.runtime.onStartup.addListener(refresh);
chrome.runtime.onInstalled.addListener(refresh);

What changed:

Change Before (MV2) After (MV3)
Background scripts + persistent: true service_worker
Toolbar API browser_action / chrome.browserAction action / chrome.action
State global count recomputed on each event
Callbacks tabs.query(cb) await tabs.query()
Startup top-level refresh() onStartup / onInstalled

Listeners stay registered at the top level so they are there whenever the worker restarts.

What this does not cover

Content scripts, webRequest → declarativeNetRequest, removing remote code, splitting host_permissions, options pages, and Chrome Web Store review. Each one is a separate job. Check the official Chrome MV3 migration guide before relying on this.

Disclosure: this article was generated by AI from a synthetic teaching example. It was first published on Hashnode.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.