
If you run Puppeteer in production long enough, you will eventually hit this error:
Error: Execution context was destroyed, most likely because of a navigation.
It is one of the most common Puppeteer failures, and the message is accurate: your script was evaluating something inside a page whose JavaScript context disappeared mid-flight, almost always because the page navigated.
Every page in Puppeteer has an execution context — the JavaScript world your `page.evaluate()` calls run in. When the page navigates (a redirect, a form submit, a client-side route change that triggers a full reload, or a meta refresh), Chrome destroys that context and creates a new one. Any `evaluate`, `$eval`, or `waitForFunction` still running against the old context throws this error.
The classic triggers:
If an action you perform causes navigation, wrap it with `waitForNavigation` — and start waiting *before* the click:
await Promise.all([
page.waitForNavigation({ waitUntil: "networkidle0" }),
page.click("a.next-page"),
]);
const title = await page.evaluate(() => document.title);
The `Promise.all` pattern matters. If you click first and call `waitForNavigation` after, the navigation can finish before the waiter attaches, and you hang or race.
For pages that redirect on load, wait until the network goes quiet before touching the page:
await page.goto("https://example.com", { waitUntil: "networkidle0" });
await page.waitForSelector("main");
const html = await page.content();
`waitForSelector` on an element you know exists post-redirect is more reliable than a fixed sleep.
Sometimes you cannot predict the navigation (third-party scripts, A/B tests). A narrow retry is pragmatic:
async function evaluateWithRetry(page, fn, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await page.evaluate(fn);
} catch (error) {
const destroyed = error.message.includes("Execution context was destroyed");
if (!destroyed || attempt === retries) throw error;
await page.waitForNetworkIdle({ idleTime: 500 });
}
}
}
If the goal is a screenshot or a PDF, you are using a manual browser-automation tool for a solved problem. A screenshot API waits for the page to settle (redirects included), renders it in managed Chrome, and returns the image — no execution contexts to race:
curl "https://api.screenshotty.link/api/v1/screenshot?url=https://example.com&ready_event=networkidle&full_page=true" \
-H "X-Api-Key: YOUR_API_KEY" \
--output screenshot.png
[Screenshotty](/) handles redirects, lazy loading, cookie banners, and ads server-side, with a free tier of 100 screenshots per month. The [full-page screenshot API](/full-page-screenshot-api) and [Node.js guide](/screenshot-api/nodejs) cover the common cases.