How to Fix “Execution Context Was Destroyed” in Playwright
Fix Playwright’s “Execution context was destroyed” error by matching your wait to the expected navigation or in-page update, then reacquiring page-bound references.
Short answer: this error usually means your code tried to evaluate JavaScript in a document that navigation had already replaced. If the action should go to a new URL, start a page.waitForURL() wait alongside the action. If the page should stay on the same URL, wait for the expected locator, response, or other observable page condition. After a document navigation, reacquire handles tied to the old document.
The error is commonly reported as Execution context was destroyed, most likely because of a navigation or page.evaluate execution context was destroyed. It is usually a timing and page-lifecycle problem; by itself, it does not prove the page is broken. Playwright documents navigation and loading as separate stages: a new URL may be committed before scripts and resources have finished loading. Playwright navigation guide
1. Decide whether the action should navigate
Start with the action immediately before the error. Look for a link click, form submission, redirect, reload, location change, or delayed script. Then decide which outcome the application is supposed to produce:
| Expected outcome | Signal to wait for | Why |
|---|---|---|
| A new document or URL | The expected URL, with page.waitForURL() |
The old document’s JavaScript context is being replaced. |
| An update in the same document | A result locator, response, or app-specific condition | There may be no navigation to wait for. |
Playwright’s automatic actionability waits help ensure an element can be interacted with, but they do not establish that every navigation or application-specific update has completed. Use a signal for the result your test needs. Playwright auto-waiting
2. Fix a click that navigates to a known URL
Arm the URL wait at the same time as the click. This avoids a timing gap if navigation begins immediately after the action.
import { test, expect } from '@playwright/test';
test('opens the account page', async ({ page }) => {
await page.goto('https://example.com');
await Promise.all([
page.waitForURL('**/account'),
page.getByRole('link', { name: 'Account' }).click(),
]);
await expect(page).toHaveURL(/\/account$/);
await expect(page.getByRole('heading', { name: 'Account' })).toBeVisible();
});
Replace the glob with the destination your application actually expects. waitForURL() accepts glob strings, regular expressions, URL patterns, and predicates, and waits for the main frame to reach a matching URL. For a query-string destination, for example, use a predicate or regular expression that checks the required path and query parameters instead of assuming a fixed order.
await Promise.all([
page.waitForURL(url => url.pathname === '/account' && url.searchParams.get('tab') === 'billing'),
page.getByRole('button', { name: 'Billing' }).click(),
]);
For an action that triggers multiple possible destinations, match the specific destination relevant to the test. A broad pattern can let the test continue after the wrong redirect.
3. Fix an update that stays on the same URL
For client-side updates, wait for an observable result rather than URL navigation. The condition should represent what the test needs to use next.
await page.getByRole('button', { name: 'Load results' }).click();
await page.locator('.results-loaded').waitFor({ state: 'visible' });
await expect(page.locator('.results-loaded')).toContainText('Results');
If the action is defined by an API response, wait for that response while triggering the action:
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/results') && response.request().method() === 'GET' && response.ok()
);
await page.getByRole('button', { name: 'Load results' }).click();
const response = await responsePromise;
await expect(page.locator('.results-loaded')).toBeVisible();
Waiting for a response alone may not mean the UI has rendered it. If the next step depends on rendered content, assert that content too. Conversely, a locator wait is usually clearer when the test cares only about visible UI.
4. Use the right wait for navigation and loading
Playwright distinguishes navigation from loading. Navigation can reach a committed URL while the page is still loading scripts, styles, or other resources. Choose the readiness signal based on what the next operation requires: the destination URL, a visible heading, a specific response, or another meaningful application condition.
Prefer page.waitForURL() for a known destination. The current Page API marks page.waitForNavigation() deprecated and describes it as inherently racy, recommending page.waitForURL() instead. Playwright Page API
Do not add a fixed sleep as a general repair. A timeout may pass on a fast run and fail on a slow one, while wasting time on every run. Playwright also discourages using networkidle as a general testing readiness signal; prefer assertions that establish the required state. Load-state API guidance
5. Reacquire references after a document change
An ElementHandle or other page-bound reference can belong to the document that just disappeared. After navigation, query the new document again instead of using a handle captured before the transition.
const oldButton = await page.getByRole('button', { name: 'Continue' }).elementHandle();
await Promise.all([
page.waitForURL('**/next-step'),
page.getByRole('button', { name: 'Continue' }).click(),
]);
// Query the current document after navigation.
const nextHeading = page.getByRole('heading', { name: 'Next step' });
await expect(nextHeading).toBeVisible();
Locators are generally preferable to retaining element handles because they resolve against the current page when used. Even with locators, make the navigation boundary explicit and assert that the new page is ready before interacting with it.
6. Complete runnable example
This Playwright Test example covers both branches: a link that navigates and a button that loads content in place. Save it as tests/navigation.spec.ts in a Playwright Test project and replace the example URLs, accessible names, and selectors with those from your application.
import { test, expect } from '@playwright/test';
test('waits for the account destination after a link click', async ({ page }) => {
await page.goto('https://example.com');
await Promise.all([
page.waitForURL('**/account'),
page.getByRole('link', { name: 'Account' }).click(),
]);
await expect(page.getByRole('heading', { name: 'Account' })).toBeVisible();
});
test('waits for an in-place result after a button click', async ({ page }) => {
await page.goto('https://example.com/search');
await page.getByRole('button', { name: 'Load results' }).click();
await expect(page.locator('[data-testid="results"]')).toBeVisible();
await expect(page.locator('[data-testid="results"]')).not.toBeEmpty();
});
Run it with npx playwright test tests/navigation.spec.ts. The example assumes the project already has Playwright Test installed and configured. The official Playwright getting-started guide covers project setup.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The error happens just after a click or submit | The action navigates while an evaluation or handle is still using the old document. | Start waitForURL() with the action, then query the new page. |
| The URL wait times out | The expected pattern is wrong, navigation did not happen, or the action failed. | Check the actual destination and whether the control triggers an in-place update. Wait for the correct URL or UI signal. |
| The URL is correct but the next locator is missing | The URL was committed before the relevant UI became ready, or the locator does not match. | Wait for the expected heading, result, or response; verify the locator against the rendered page. |
| A locator or handle fails after redirect | The reference was tied to the prior document. | Reacquire it from page after the navigation wait. |
| A catch block sometimes sees the old URL | The failure and URL observation can race with the transition; a post-error URL check is not a reliable synchronization method. | Synchronize before evaluation rather than guessing after the error. |
networkidle or a long sleep makes the test flaky |
Neither signal necessarily represents the application state needed by the test. | Wait for the expected URL, locator, or response and assert the condition required by the next step. |
| The error follows logout or an external redirect | The redirect replaces the document before a subsequent evaluation, such as reading session storage. | Wait for the expected redirect destination. If the evaluation is required, run it in the correct page context and only when that context exists. |
8. When can you catch and ignore the error?
Only catch it when the interrupted evaluation is genuinely optional and losing its result cannot make the test pass incorrectly. For example, best-effort page cleanup may be skipped if the page is already leaving. If the evaluation supplies data needed for an assertion or later action, do not suppress the error: fix the synchronization or test logic so the required result is obtained from the right document.
A reported Playwright issue illustrates the pattern: a logout redirects to another domain, followed by an evaluation against the prior page context. Issue reports are useful examples of failure timing, but are not by themselves proof of a general Playwright defect. Playwright issue #27406
9. Reliability and performance notes
- Make waits specific. A destination URL or meaningful UI state makes failures easier to diagnose than a generic delay.
- Start the wait before the transition can win the race. Pair the wait and triggering action with
Promise.all. - Wait only as far as the next step requires. If the next action needs a visible heading, assert that heading; there is no need to wait for unrelated resources.
- Use bounded waits. Playwright waits have timeouts; when one expires, investigate whether the expected event occurred and whether the matcher describes it correctly. Increasing a timeout can accommodate a known slow operation, but it does not correct a wrong signal.
- Keep test state deterministic. Redirects, delayed scripts, authentication state, and third-party resources can change which outcome occurs. Assert the expected destination and application state explicitly.
10. Or skip the browser setup
If your goal is to capture a website screenshot or PDF rather than test a browser interaction, ScreenshotNeo provides a single-request screenshot API and an MCP server for AI agents. It is made by Yorker Media. See the ScreenshotNeo website and API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', await res.arrayBuffer());
For other Node.js runtimes, write the response bytes with the runtime’s file API. Set your API key as a secret in your environment rather than committing it to source control. ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before the shot; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies page verdict and billing status in headers. Its MCP server includes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is on every plan.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
11. Frequently asked questions
Does this error always mean my page is broken?
No. It often means an operation overlapped with a document replacement. Inspect the action and the expected page transition before treating it as an application defect.
Is waitForNavigation() the right fix?
Use waitForURL() when you know the expected destination. The Page API marks waitForNavigation() deprecated and inherently racy.
Should I retry the failed evaluation?
Only after ensuring it will run in the intended document and that repeating it is safe. Usually the robust fix is to wait for the intended transition first, then evaluate or query the new page.
What if the URL does not change?
Wait for a locator, response, or page condition that proves the in-place update finished. A navigation wait cannot establish a state that does not involve navigation.


