Short answer
The core problem is the same as in Puppeteer: your code starts navigation and then tries to use the old page context after the browser already replaced it.
In Playwright, prefer page.waitForURL() over page.waitForNavigation(), because page.waitForNavigation() is deprecated and inherently racy.
await Promise.all([ page.waitForURL("**/dashboard"), page.getByRole("link", { name: "Dashboard" }).click(),]);If the action does not navigate, do not wait for a URL change. Wait for a locator, response, or another concrete signal instead.
If you are using Puppeteer rather than Playwright, follow the matching guide on how to fix this error in Puppeteer.
Why this error happens
Playwright runs page.evaluate() and related calls inside the current page execution context. When the page navigates, reloads, redirects, or replaces the frame, the old execution context is destroyed and a new one is created for the new document.

In browser internals, an execution context is the JavaScript environment bound to a specific document, frame, or isolated world. When you call page.evaluate(), Playwright sends your function to the browser and asks it to run in that exact context. This is not the same thing as Playwright’s BrowserContext, which is an isolated browser session with its own cookies, permissions, and storage.
That distinction matters because the execution context belongs to the current document. After a redirect or reload, any pending page.evaluate() call, element handle, or JS handle from the previous document can become invalid immediately.
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, consent scripts, 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, wait for the URL change at the same time as the action:
await Promise.all([ page.waitForURL("**/account", { timeout: 30_000 }), page.getByRole("link", { name: "Account" }).click(),]);For a form submit:
await page.getByLabel("Email").fill("john@example.com");await page.getByLabel("Password").fill("secret");
await Promise.all([ page.waitForURL("**/dashboard"), page.getByRole("button", { name: "Sign in" }).click(),]);For script-triggered navigation:
const currentUrl = page.url();
await Promise.all([ page.waitForURL((url) => url.toString() !== currentUrl), page.evaluate(() => { window.location.href = "https://example.com/dashboard"; }),]);If you await the click first and only then start waiting for the URL, you can miss the transition and end up with the same race condition.
About waitForNavigation in Playwright
Playwright still has page.waitForNavigation(), but it is deprecated. The Playwright docs explicitly recommend page.waitForURL() instead because waitForNavigation() is inherently racy.
That is one of the main differences from Puppeteer guidance. The root cause is the same, but the Playwright-native fix should use waitForURL(), locators, and Playwright’s built-in auto-waiting model.
Fix page.evaluate errors
The page.evaluate variant is a common version of this issue:
await page.getByRole("link", { name: "Pricing" }).click();await page.evaluate(() => document.title);If the click starts navigation, the evaluate call can target a context that no longer exists.
Wait for the new page first:
await Promise.all([ page.waitForURL("**/pricing"), page.getByRole("link", { name: "Pricing" }).click(),]);
const title = await page.evaluate(() => document.title);console.log(title);The same applies to element handles and JS handles created before navigation. Reacquire them on the new page instead of reusing old handles from the previous document.
In some flows, page.evaluate() is only a best-effort step, such as removing a popup, collecting analytics data, or reading a non-critical value. In those cases, it can be acceptable 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 can be useful when an unknown inline script, A/B test, consent banner, or third-party redirect interrupts your automation unexpectedly.
When waitForURL is the wrong fix
Not every page update changes the URL.
If the site updates content with client-side JavaScript and stays on the same page, page.waitForURL() is the wrong tool. Use a more specific wait instead.
Wait for a locator:
await page.getByRole("button", { name: "Load results" }).click();await page.locator(".results-loaded").waitFor({ state: "visible", timeout: 30_000,});Wait for a response:
await page.getByRole("button", { name: "Refresh data" }).click();await page.waitForResponse((response) => { return response.url().includes("/api/results") && response.ok();});Wait for a page condition:
await page.getByRole("button", { name: "Show price" }).click();await page.waitForFunction(() => { return document.querySelector(".price")?.textContent?.trim().length > 0;});Playwright already auto-waits for many actions through locators, but auto-waiting does not solve every navigation race or every asynchronous page update. You still need the right wait primitive for the event you actually expect.
Common mistakes
- Using
page.waitForNavigation()in new Playwright code instead ofpage.waitForURL(). - Waiting for the URL only after the click already happened.
- Assuming Playwright auto-waiting will handle redirects triggered by custom scripts or delayed navigation.
- Calling
page.evaluate()or reusing stale handles after reload or redirect. - Waiting for a URL change when the page never actually changes URL.
Related reading
If you need a broader walkthrough of Playwright’s waiting model, auto-waiting, and screenshot flows, follow how to render screenshots with Playwright.
If you are working with Puppeteer instead, the matching article is how to fix “Execution context was destroyed” in Puppeteer.
Or use an API
If this error shows up in a screenshot rendering pipeline, all this synchronization is already built into our ScreenshotOne URL to Image API.
You can start for free.
