MV3 error categories
MV3 produces a small set of error messages over and over, from wildly different call sites. A generic tracker groups by stack trace, so one root cause becomes two hundred issues.
CrxTrace classifies each event into an mv3Category and groups by cause
rather than call site. One underlying problem is one issue.
The categories
Section titled “The categories”| Category | Typical message | What it actually means |
|---|---|---|
context_invalidated |
Extension context invalidated |
The extension was reloaded, updated, or disabled while a content script from the old version was still running on a page. |
no_receiver |
Could not establish connection. Receiving end does not exist. |
You sent a message to something that isn’t listening — usually a content script not injected on that page, or a worker that hasn’t booted. |
message_port_closed |
The message port closed before a response was received. |
A sendMessage listener returned without responding, or forgot to return true for an async response. |
sw_terminated |
(synthesised) | The service worker was killed with tracked work still in flight. See Service worker deaths. |
storage_quota |
QUOTA_BYTES quota exceeded |
You’ve exceeded chrome.storage limits. sync is far smaller than local. |
permission_denied |
Cannot access contents of the page |
A missing host permission, or a page extensions may not touch at all. |
script_injection |
Cannot access a chrome:// URL |
chrome.scripting aimed at a restricted page — chrome://, the Web Store, or another extension. |
tab_gone |
No tab with id: 42 |
The tab closed or navigated between you reading its id and using it. |
native_host |
Specified native messaging host not found |
The native messaging host isn’t installed or isn’t registered for your extension id. |
csp |
Refused to execute inline script |
The page’s Content Security Policy blocked something your content script did. |
network |
Failed to fetch |
A request failed — offline, DNS, CORS, or the server. |
user_gesture |
must be called during a user gesture |
An API that requires a real click was called after an await, which discards gesture context. |
unknown |
— | Everything else, including your own application errors. |
Why grouping by cause matters
Section titled “Why grouping by cause matters”Take no_receiver. In a real extension it fires from every place you call
chrome.tabs.sendMessage — a dozen call sites, all with different stacks, all
the same root cause: you messaged a tab with no content script.
Grouped by stack, that’s a dozen issues that each look mildly puzzling. Grouped by cause, it’s one issue with a clear fix.
The same applies to context_invalidated, which fires from wherever your
content script happened to be when the user hit reload — effectively a random
stack every time.
The two you should fix first
Section titled “The two you should fix first”message_port_closed is almost always a real bug in your message handling:
// Broken — the listener returns synchronously, so the port closeschrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { doAsyncWork().then(sendResponse);});
// Fixed — return true to keep the port openchrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { doAsyncWork().then(sendResponse); return true;});user_gesture is a bug that only shows up on slow machines, which is why
you never see it locally:
// Broken — the gesture is gone by the time this runsbutton.addEventListener("click", async () => { await loadSettings(); chrome.tabs.create({ url }); // may throw});
// Fixed — use the gesture first, await afterbutton.addEventListener("click", () => { chrome.tabs.create({ url }); void loadSettings();});Expected noise
Section titled “Expected noise”Some of these are unavoidable and not worth alerting on:
context_invalidatedspikes every time you ship an update — old content scripts are still on open tabs. A spike right after a release is normal; a steady stream is not.no_receiveris routine if you broadcast to tabs speculatively.networktracks your users’ connectivity as much as your code.
If a category is pure noise for your extension, drop it:
CrxTrace.init({ dsn: "...", ignoreErrors: [/Extension context invalidated/],});Better, keep it but stop alerting on it — the volume is a useful signal even when individual events aren’t.
Reading the category in code
Section titled “Reading the category in code”CrxTrace.init({ dsn: "...", beforeSend: (event) => { if (event.mv3Category === "network" && !navigator.onLine) return null; return event; },});Related
Section titled “Related”- Filtering events
- Envelope format — where
mv3Categorysits on the wire