Service worker and messaging
Debug each context
Open the correct DevTools target for popup, service worker, content script, and page instead of looking for every log in one console.
There is no single console for an extension. Each context has its own DevTools, its own logs, and its own lifecycle. Half of extension debugging is opening the right window.
Where each console lives
The popup: open it, right-click inside it, choose Inspect. A DevTools window opens for that popup document.
The service worker: go to chrome://extensions and click the “service worker” link on the extension card. If the link says “inactive”, the worker is stopped. Clicking it wakes it up.
The content script: it runs inside the page, so open the page’s DevTools. In the Console panel there’s a context dropdown that says “top” by default. Switch it to Page Notes. Now chrome.runtime exists and you see your script’s logs.
Manifest errors and uncaught runtime errors: the Errors button on the extension card.
Inspectors change behavior
Here’s the catch. Opening DevTools on the popup keeps it open even when it loses focus. That’s convenient, and it also hides every bug related to the popup closing. Repeat the important checks with DevTools closed.
The worker inspector does the same. While it’s open, Chrome keeps the worker alive, so you never see the cold-start path where globals are reset. Record what you need, close the inspector, wait for the worker to show “inactive”, and fire the event again.
Which instance wrote that log
After a reload, the page’s console still shows lines from the old content script, followed by lines from the new one. They look identical. This is why we log the extension version in each script since lesson three.
For durable state, don’t read console logs at all. Read storage. In the worker console:
await chrome.storage.local.get(null)
That returns everything the extension stored, whatever instance wrote it.
One ID across three consoles
When a single click travels popup → worker → content script, three consoles each show a fragment. Give the action a short ID at the start and pass it along in every message:
const actionId = crypto.randomUUID().slice(0, 8)
console.log(`[${actionId}] popup: highlight clicked`)
Now you can line up the three logs by ID and read one trace. Log the ID and the step. Don’t log the note text or the full URL, those are the user’s data.
Clear the consoles and reload before each attempt, so you’re never reading yesterday’s evidence.
Try this now. Trigger one deliberate error in each context: a typo in popup.js, a throw in the worker listener, an undefined variable in content.js. Then reload the extension and send a message to a tab that wasn’t reloaded. For each case write down which console showed it, the version that was running, and the reload sequence that fixed it. That table is your debugging map for the rest of the project.
Lesson completed