Debugging a Popup Before It Closes
Debug an extension popup that disappears whenever you click into DevTools: Inspect popup, opening popup.html in a tab, breaking on startup, logging early errors, preserving the popup during debugging, and automated popup tests.
Table of Contents
- Why popups are hard to debug
- Step-by-step: debugging popups
- 1. Use Inspect popup for interactive debugging
- 2. Open the popup page in a tab for startup problems
- 3. Reload with DevTools attached to catch early errors
- 4. Pause at startup deliberately
- 5. Check the Errors page and the console of the right context
- 6. Log startup errors somewhere that outlives the popup
- 7. Debug layout at the real size
- 8. Reproduce in an automated test
- Common mistakes
- Cross-browser variation
- Verification
- FAQ
- Related
The popup shows a blank white box for a fraction of a second and closes. Or it opens fine, but as soon as you click into DevTools to set a breakpoint, the popup closes and takes its DevTools window with it. Popups close on blur by design, which makes them the most awkward extension context to debug: errors during startup happen before you can open DevTools, and interactive debugging fights the close-on-blur behaviour. There are reliable techniques for each problem — inspecting through the toolbar, loading the popup as a tab, pausing on startup, and capturing errors somewhere that outlives the popup. This guide collects them. It belongs to debugging extension contexts.
Why popups are hard to debug
A popup is an extension page shown in a browser-owned bubble. It is created when the user clicks the action and destroyed when it loses focus — including focus moving to a separate DevTools window. While DevTools is attached through “Inspect popup”, Chrome keeps the popup open even when focus moves to DevTools, which is the main trick. Startup problems are different: a script error, a CSP violation or a failed import happens in the first milliseconds, often leaving a blank popup that closes before any DevTools can attach. For those, you need the errors captured elsewhere or a way to run the same page where it does not close.
Step-by-step: debugging popups
1. Use Inspect popup for interactive debugging
Right-click the extension’s toolbar icon and choose Inspect popup (in Chrome and Edge; the item appears for unpacked extensions and when developer mode is on). The popup opens with DevTools attached, and it stays open while DevTools has focus, so you can set breakpoints, step through code and edit the DOM. Closing DevTools closes the popup. This is the right tool for everything except startup errors.
2. Open the popup page in a tab for startup problems
1chrome-extension://<extension-id>/popup.html
Execution context: a normal browser tab. Find the ID on chrome://extensions (or log chrome.runtime.id). As a tab, the page never closes on blur, DevTools can be opened before the page loads (open DevTools, then reload), and the Console shows every startup error. Differences to remember: there is no activeTab grant, chrome.tabs.query({active: true, currentWindow: true}) returns the popup’s own tab, and window.close() closes the tab. Pass a test tab ID through the URL if the popup needs one.
3. Reload with DevTools attached to catch early errors
With the popup open via Inspect popup, press Ctrl+R / ⌘R in its DevTools window. The popup document reloads while DevTools stays attached, so the Console and Sources panels capture everything from the first line. Combine with “Pause on uncaught exceptions” in the Sources panel to stop exactly where startup fails.
4. Pause at startup deliberately
1// popup.js — temporary, development only
2if (new URLSearchParams(location.search).has("debug")) debugger;
Execution context: the popup page. Opening popup.html?debug in a tab with DevTools open pauses at the first line, before any of your initialisation runs. In the real popup, a debugger statement only pauses if DevTools is already attached, so it is harmless if left behind, but remove it before release.
5. Check the Errors page and the console of the right context
On chrome://extensions, the extension’s Errors button lists uncaught errors and console errors from all contexts, including popups that have already closed, with stack traces and the context they came from. Errors from the popup do not appear in the service worker’s console — each context has its own. See reading errors from the extensions page.
6. Log startup errors somewhere that outlives the popup
1// popup-errors.js — loaded first in popup.html
2addEventListener("error", (e) => persist({ msg: e.message, src: e.filename, line: e.lineno, stack: e.error?.stack }));
3addEventListener("unhandledrejection", (e) => persist({ msg: String(e.reason), stack: e.reason?.stack }));
4function persist(entry) {
5 chrome.storage.session.get({ popupErrors: [] }).then(({ popupErrors }) =>
6 chrome.storage.session.set({ popupErrors: [...popupErrors.slice(-19), { ...entry, at: Date.now() }] }));
7}
Execution context: the popup, loaded as the first script. Persisting the last 20 errors in storage.session lets you read them from the service worker console or the options page after the popup has closed — invaluable for intermittent failures. In production builds, send them to your error reporting instead; see capturing uncaught errors in every context.
7. Debug layout at the real size
Open popup.html in a tab, open DevTools, and use the device toolbar to set a custom size such as 380×560 — or set the tab’s window size with a small script. Chrome popups size to their content within limits (up to 800×600), so layout bugs often only appear at the real constraints. See fixing popup size and overflow issues.
8. Reproduce in an automated test
1// Playwright
2const popup = await context.newPage();
3await popup.goto(`chrome-extension://${extensionId}/popup.html`);
4const errors = [];
5popup.on("pageerror", (e) => errors.push(e));
6await popup.reload();
7expect(errors).toEqual([]);
Execution context: an end-to-end test. Loading the popup page in a tab under Playwright gives a reproducible environment for startup errors, and a test that asserts no page errors catches regressions. See testing a popup and options page with Playwright.
Common mistakes
- Clicking into DevTools without Inspect popup. The popup closes.
- Looking in the service worker console for popup errors. Each context has its own console.
- Assuming the tab behaves exactly like the popup. No
activeTab, different active tab. - Missing startup errors. Reload with DevTools attached, or persist errors.
- Leaving
debuggerstatements in releases. Remove them.
Cross-browser variation
- Chrome / Edge: Inspect popup on the toolbar icon; Errors page on
chrome://extensions. - Firefox: open
about:debugging, Inspect the extension, and enable “Disable popup auto-hide” from the toolbox’s ⋯ menu to keep popups open while debugging. - Safari: enable the Develop menu, then Develop → Web Extension Background Content / the extension’s pages; popovers can be inspected via Web Inspector, and keeping them open may need the inspector attached first.
Verification
- Introduce a deliberate startup error and confirm it appears on the Errors page and in the tab console.
- Inspect popup, set a breakpoint in a click handler and confirm the popup stays open while paused.
- Reload the inspected popup and confirm startup logs appear.
- Confirm the persisted error list contains the last popup error.
FAQ
Why does Inspect popup not appear?
It appears for extensions loaded unpacked or with developer mode on. Packed store installs may not offer it.
Can I keep the popup open without DevTools?
Not in Chrome. Firefox’s “Disable popup auto-hide” is the closest equivalent.
Why does the popup work in a tab but not as a popup?
Usually because of activeTab-dependent code, the active tab query, or size constraints. Test those paths in the real popup.
Do console logs from the popup persist after it closes?
Not in its console — that DevTools window closes with it. Errors still appear on the extensions Errors page, and anything you persisted yourself remains in storage.
Related
- Finding the right DevTools target for each context — where each context is inspected.
- Reading errors from the extensions page — after-the-fact errors.
- Preserving popup state when it closes — designing for closing.
- Debugging extension contexts — the parent topic.