An unhandled Puppeteer rejection is fixed where the failed promise is created: await the operation inside a meaningful try…catch, return the promise to its caller, or attach .catch() to the exact chain. Then inspect the rejection reason and determine whether the failure came from Node code, JavaScript running in the page, or the browser protocol. A process-level listener is useful for diagnosis, but it does not repair the operation.
What an “unhandled rejection” means in Puppeteer
Puppeteer calls such as browser.newPage(), page.goto(), and page.click() return promises. Node.js emits unhandledRejection when a promise is rejected and no error handler is attached within a turn of the event loop. The event provides the rejection reason and the promise. If a handler is attached later, Node can emit rejectionHandled.
This is a Node runtime condition, not a Puppeteer-specific exception class. A rejection may also be produced by a promise farther down a chain: a callback passed to .then() can throw, creating a new rejected promise. That resulting promise must itself be returned or handled.
The reliable repair pattern
Await operations inside an async workflow
Put related browser work in an async function and await every promise that can fail. Handle the workflow at a boundary where you can log, retry, or report the error.
#1 Best Overall
const puppeteer = require('puppeteer');
async function run() {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', {waitUntil: 'networkidle2'});
await page.screenshot({path: 'example.png', fullPage: true});
} finally {
await browser.close();
}
}
run().catch(error => {
console.error('Puppeteer task failed:', error);
process.exitCode = 1;
});
The top-level run().catch() owns failures that escape the workflow. The finally block attempts cleanup even when navigation or capture fails. In production, decide how to handle a cleanup failure so it does not hide the original error.
Use a catch on a promise chain
If you use promise chaining instead of async/await, return nested promises and attach .catch() to the chain that you intend to observe.
function capture() {
return puppeteer.launch()
.then(browser => browser.newPage().then(page => ({browser, page})))
.then(({browser, page}) => page.goto('https://example.com')
.then(() => page.screenshot({path: 'example.png'}))
.finally(() => browser.close()));
}
capture().catch(error => {
console.error('Capture failed:', error);
});
A common bug is starting an asynchronous callback without returning it:
// The outer chain finishes before the inner rejection is attached.
items.forEach(item => {
doAsyncWork(item); // rejection can become unhandled
});
// Prefer a loop when work must be awaited.
for (const item of items) {
await doAsyncWork(item);
}
For independent work, use Promise.all() and catch its result. If one task failing should not cancel the others, use Promise.allSettled() and inspect each status.
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #2
Find the promise that lost its handler
- Log the complete reason. Preserve the error object and stack; do not log only
error.message. - Search every promise boundary. Check Puppeteer calls,
.then()callbacks, timers, array iteration methods, and event listeners. An outer catch cannot observe work that was detached. - Choose a policy. At the local boundary, decide whether to retry, skip, close the browser, or fail the job. Do not silently swallow the rejection.
- Verify cleanup. Browser shutdown is asynchronous and can reject too. Keep cleanup in
finallyand preserve the primary failure when reporting both errors.
Observing process-level events
During diagnosis, you can record unhandled rejections:
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection:', reason);
console.error('Promise:', promise);
});
process.on('rejectionHandled', promise => {
console.warn('A rejection gained a handler later:', promise);
});
These listeners show what escaped normal handling; they do not make the failed Puppeteer operation successful. Node documents that an unhandled rejection that remains unhandled can be raised as an uncaught exception, and command-line policy can be changed with --unhandled-rejections. Treat global hooks as monitoring, not normal recovery.
Separate Node errors from page errors
Puppeteer has two JavaScript environments: your Node “server” code and the page’s browser “client” code. A page script throwing does not automatically appear as a Node promise rejection.
Relay browser console output
page.on('console', message => {
console.log(`[page ${message.type()}]`, message.text());
});
This listener forwards messages emitted by console.log, console.error, and related page APIs to your Node logs.
Rank #3
Listen for page exceptions
page.on('pageerror', error => {
console.error('Page JavaScript error:', error);
});
The pageerror event is separate from Node’s unhandledRejection. Inspect both when navigation appears successful but page behavior is broken. Event payload details can vary by Puppeteer release; verify them against the version in your lockfile and the PageEvents API.
Debugging the underlying cause
Node-side debugging
Use Node’s inspector when the failure is in your script or a Puppeteer call made from Node. Set breakpoints around the call and inspect the rejection stack.
Browser-side debugging
For code executed with page.evaluate() or loaded by the site, enable browser developer tools and use a debugger statement in the page context. Relay console output and listen for pageerror so browser failures are visible in your terminal.
Protocol traffic and pending calls
Puppeteer’s debugging guide documents NODE_DEBUG="puppeteer:*" for protocol logging. If an asynchronous call never resolves, inspect browser.debugInfo.pendingProtocolErrors where supported. These tools diagnose protocol problems; they are not substitutes for handling the promise.
Rank #4
Request interception: the asynchronous handler trap
When request interception is enabled, each request stalls until a handler continues, responds, or aborts it. Puppeteer’s request-interception guide documents returning a promise from an asynchronous handler so Puppeteer can await the handler.
await page.setRequestInterception(true);
page.on('request', async request => {
try {
await checkPolicy(request.url());
// Another listener may have resolved it while we awaited.
if (request.isInterceptResolutionHandled()) return;
await request.continue();
} catch (error) {
console.error('Interception policy failed:', error);
if (!request.isInterceptResolutionHandled()) {
await request.abort();
}
}
});
Recheck request.isInterceptResolutionHandled() immediately before continue, respond, or abort. The check and resolution must be synchronous together after the await. This prevents a race in which another listener has already resolved the request. It does not replace catching rejection from your own asynchronous policy function. See the request interception guide.
Common symptoms and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
Process prints UnhandledPromiseRejection after a click or navigation |
The Puppeteer promise was started but not awaited or returned | Await it in the workflow or return the chain and attach .catch() |
| Top-level catch never runs | An event callback, timer, or forEach callback detached its promise |
Return the callback’s promise where supported, or move work into an awaited loop |
| Navigation succeeds but the page is broken | Exception occurred in page JavaScript | Listen for pageerror and relay the page’s console event |
| “Request is already handled” or interception resolution error | Another listener resolved the request during an asynchronous wait | Recheck isInterceptResolutionHandled() immediately before resolving |
| Browser hangs with pending calls | A protocol operation is waiting or a handler stalled interception | Enable NODE_DEBUG="puppeteer:*", inspect pending protocol errors, and ensure every intercepted request is resolved |
| Adding a global listener appears to “fix” crashes | The listener only observes or changes process behavior | Repair the owning promise and keep global events for logging and monitoring |
Reliability and error-policy checklist
- Keep one clear owner for each Puppeteer promise.
- Await browser creation, page creation, navigation, actions, evaluations, screenshots, PDFs, and shutdown.
- Use
finallyfor cleanup, with an explicit policy for cleanup errors. - Include URL, operation name, and the original stack in logs.
- Set timeouts deliberately and handle timeout errors at the workflow boundary.
- Do not continue normal service operation after an uncaught exception merely because a listener logged it; Node warns that resuming after
uncaughtExceptionis unsafe. - Check your installed Puppeteer version. The examples here align with documentation for Puppeteer 25.12.0 request interception and debugging and 25.11.0 PageEvents; APIs can change.
Or skip the browser setup
If your goal is simply a clean image or PDF rather than browser automation code, ScreenshotNeo provides a website screenshot API and MCP server. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status.
One GET request is enough:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for the 63 capture options, including full-page lazy-image loading, CSS-selector element capture, device presets, dark mode, retina scale, PDF settings, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous webhooks, bulk capture, usage data, and the OpenAPI specification.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallIt also exposes an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots each month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Should I add process.on('unhandledRejection') to every script?
Use it when you need process-level diagnostics or monitoring. It should not replace local await/try…catch handling or be treated as proof that the failed operation recovered.
Why does a rejection appear even though I have a catch?
The catch may be attached to a different promise. A .then() callback, timer, event listener, or array iteration can create a new promise that is never returned to the chain you catch.
Can request interception cause an unhandled rejection?
Yes. The handler’s asynchronous policy work can reject, and a separate race can occur if another listener resolves the request while it is awaiting. Catch the policy error and recheck interception state before resolving.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsWhich version should I follow?
Match your package lockfile. The documented behavior referenced here is from Node.js v26.10.0 documentation and Puppeteer documentation versions 25.12.0 and 25.11.0; confirm details for newer releases.
Frequently Asked Questions
Is an unhandled rejection the same as an uncaught exception?
No. An unhandled rejection concerns a rejected promise without a timely handler. Node may later raise it as an uncaught exception according to its rejection policy, but the terms describe different stages and signals.
Should I retry every failed Puppeteer call?
No. Retry only errors your application classifies as transient, such as a selected navigation timeout. Authentication failures, invalid selectors, and page-code bugs generally require a fix rather than an automatic retry.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.




