Short answer
The most common reason for this error is a race condition: your code clicks a link, submits a form, or changes window.location, and then tries to read or modify the old page context after navigation already started.
The safest fix is to start waiting for navigation before the action that triggers it:
const [response] = await Promise.all([ page.waitForNavigation({ waitUntil: ["domcontentloaded", "networkidle2"], }), page.click("a"),]);Use the same pattern when a form submit, redirect, reload, or script-triggered navigation is expected.
If your code does not actually navigate, page.waitForNavigation() is the wrong tool. In that case, wait for a selector, response, function, or another concrete signal instead.
For more details on waitUntil, load, domcontentloaded, and networkidle2, follow the guide on how to wait for page load in Puppeteer.
If you use Playwright instead, there is now a dedicated Playwright version of this guide: how to fix “Execution context was destroyed” in Playwright.
Why this error happens
Puppeteer runs functions like page.evaluate() and page.$eval() inside the current page execution context. When the page navigates, reloads, or replaces the frame, the old context is destroyed and Puppeteer can no longer use it.

In Puppeteer internals, an execution context is the JavaScript environment for a specific page, frame, or isolated world. When you call page.evaluate(), Puppeteer serializes your function, sends it to Chrome DevTools Protocol, and asks Chrome to run it in that exact execution context. This is different from Puppeteer’s BrowserContext, which is about isolated browser storage like cookies and local storage.
That distinction matters because the execution context is tied to the current document. As soon as the page navigates, reloads, redirects, or swaps frames, Chrome destroys the old context and creates a new one for the new document. Any pending page.evaluate() call, element handle, or JS object handle from the previous page can become invalid at that moment, which is why Puppeteer throws this error instead of letting you keep talking to a page that no longer exists.
Typical triggers are:
- clicking a link that opens the next page;
- submitting a form;
- calling
window.location = "..."orlocation.reload(); - unknown page scripts, third-party widgets, or tag manager code that trigger a redirect for you;
- redirects after login or checkout;
- evaluating JavaScript while the page is in the middle of navigation.
The error often appears as one of these messages:
Execution context was destroyed, most likely because of a navigationpage.evaluate: Execution context was destroyed, most likely because of a navigation
Fix it after click, submit, or redirect
When an action triggers navigation, pair it with page.waitForNavigation() in the same Promise.all() call:
const [response] = await Promise.all([ page.waitForNavigation({ waitUntil: ["domcontentloaded", "networkidle2"], timeout: 30_000, }), page.click("a.my-link"),]);
console.log(response?.status());For a form submit:
await page.type('input[name="email"]', "john@example.com");await page.type('input[name="password"]', "secret");
await Promise.all([ page.waitForNavigation({ waitUntil: ["domcontentloaded", "networkidle2"], }), page.click('button[type="submit"]'),]);For script-triggered navigation:
await Promise.all([ page.waitForNavigation({ waitUntil: ["load", "networkidle2"], }), page.evaluate(() => { window.location.href = "https://example.com/dashboard"; }),]);This matters because page.click() can trigger navigation immediately. If you await page.click() first and only then call page.waitForNavigation(), you can miss the navigation event and get the race condition you were trying to avoid.
Fix page.evaluate errors
This page should cover page.evaluate explicitly because many searches are for the page.evaluate: execution context was destroyed variant.
The problem usually looks like this:
await page.click("a");await page.evaluate(() => { return document.title;});If the click starts navigation, the evaluate call can run against a page context that no longer exists.
Fix it by waiting for the new page state first:
await Promise.all([ page.waitForNavigation({ waitUntil: ["domcontentloaded", "networkidle2"], }), page.click("a"),]);
const title = await page.evaluate(() => document.title);console.log(title);The same rule applies to page.$eval(), page.$$eval(), and element handles from the old page. After navigation, reacquire the elements on the new page instead of reusing stale handles.
In some flows, page.evaluate() is only a best-effort step, for example analytics extraction, cosmetic DOM cleanup, or reading a value you do not strictly need. In those cases, it can be reasonable to ignore this specific failure and continue:
try { await page.evaluate(() => { document.querySelector(".popup")?.remove(); });} catch (error) { // Ignore execution context errors if this step is optional.}This is especially useful when the redirect is not triggered by your code directly, but by an unknown inline script, A/B test, consent banner, or third-party script that changes the page while your automation is still running.
When waitForNavigation is the wrong fix
Not every page update is a navigation.
If a site updates content with client-side JavaScript and stays on the same page, page.waitForNavigation() can just hang until timeout. Use a more specific wait instead:
Wait for an element:
await page.click("button.load-results");await page.waitForSelector(".results-loaded", { visible: true, timeout: 30_000,});Wait for a response:
await page.click("button.refresh-data");await page.waitForResponse((response) => { return response.url().includes("/api/results") && response.ok();});Wait for a page condition:
await page.click("button.show-price");await page.waitForFunction(() => { return document.querySelector(".price")?.textContent?.trim().length > 0;});If you are unsure which wait is correct, start by asking: did the page actually navigate, or did it only update part of the DOM?
Common mistakes
- Waiting for navigation after the click instead of at the same time.
- Using
page.waitForNavigation()when there is no real navigation. - Calling
page.evaluate()or reusing element handles from the old page after reload or redirect. - Waiting only for
loadon modern pages that still render important content after initial load. - Forgetting to set a timeout and ending up with scripts that hang too long.
For navigation-heavy screenshot flows, the companion guide on how to wait for page load in Puppeteer explains which waitUntil values are usually safer.
If you are taking screenshots
If this error happens while taking screenshots, the root cause is usually the same: the page changed context before your script finished interacting with it.
The fix is still to synchronize navigation and page actions correctly. If you want a broader walkthrough, see how to take a screenshot with Puppeteer.
Or use an API
If you encounter this error while building a screenshot rendering pipeline, all these issues and this synchronization is already built into our ScreenshotOne URL to Image API.
And in addition to that we solve most if not all the issues related to rendering screenshots in headless browsers including blocking ads, banners, rendering lazy-loaded elements, and more.
You can start for free.
