You change "manifest_version": 2 to 3 and the extension breaks. Two errors show up a lot:
Service worker registration failedchrome.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.
