When Automation Fails, Find the Boundary

When a Puppeteer run fails, the visible symptom often arrives far from the cause. A missing browser can look like a broken page action; an incompatible browser can look like a protocol bug; a page-side promise can look like a frozen Node process. This chapter focuses on diagnosing those boundaries in order, from browser acquisition and version identity through connection, Node code, page code, the browser process, and protocol telemetry. The result is a practical judgment: verify the earliest failed boundary before changing the page logic, and collect enough evidence to distinguish repairable setup from a real capability limit.

Puppeteer’s debugging guidance starts from a useful separation. There is server code running in Node, client code running in the browser page, and browser or internal code underneath them. The protocol connection cuts across these layers: it carries commands and events, and it can fail while the page itself remains healthy. A good diagnosis does not treat “the script hung” as a cause. It asks which boundary stopped making progress and what observation could confirm that answer.

First verify the browser you are actually running

The earliest failure is often outside the page. The puppeteer product package installs a compatible browser as part of its normal workflow, while puppeteer-core assumes that the caller supplies or manages the browser. Modern package managers can block dependency install scripts. In that situation, the package may be present but the expected browser was never downloaded, and the first launch can fail with a missing-browser error.

That symptom should not be repaired by changing a selector. First identify browser ownership. If the product package is responsible, check whether its install step ran and whether its cache contains the expected build. The documented remedy is to install the browser explicitly after package installation or configure the package manager to permit the script. If core is being used, check the explicit executable path or browser channel instead. Core ignores Puppeteer’s configuration files and environment variables, so copying a product-level configuration into a core-only deployment does not transfer that responsibility.

The cache is part of this diagnosis. Puppeteer stores managed browsers in a cache by default, and configuration can change the cache directory or request multiple browser products. The @puppeteer/browsers tooling can install, list, launch, and clear cached builds, which makes browser identity inspectable rather than an invisible side effect of dependency installation. A custom download provider can point at a mirror, but the project places compatibility, testing, and maintenance responsibility for custom providers on the caller; Puppeteer’s official guarantee is for Chrome for Testing binaries.

A browser that exists can still be the wrong browser. Puppeteer couples releases to browser builds because protocol changes can break an otherwise valid API. In the captured repository, Puppeteer 25.11.0 is paired with Chrome for Testing 153.0.8010.36 and Firefox 155.0.1. Those values describe the repository snapshot, not a permanent promise for every executable path. If a system-installed browser behaves differently from the managed build, record its identity before investigating page code. “It launched” proves process startup, not protocol compatibility.

Mode is another version-adjacent boundary. Current Puppeteer uses regular Chrome headless mode by default. The older headless implementation is a separate chrome-headless-shell binary, selected with headless: 'shell'. The documentation says shell mode may be more performant when the full Chrome feature set is unnecessary, but it does not match regular Chrome completely.