# "Execution context was destroyed" in Puppeteer and Playwright: cause and fixes

> Why Puppeteer and Playwright throw "Execution context was destroyed, most likely because of a navigation", reproduced on a test page, with four fixes.

Published 2026-11-08 · https://shareglint.com/en/guides/puppeteer-execution-context-destroyed

"Execution context was destroyed, most likely because of a navigation" is one of the most searched Puppeteer errors, and Playwright prints the same sentence. The message is accurate: something in your script was running in a page when the browser threw that page away for a new one. This page reproduces it on a small test site, shows where it comes from, and gives four fixes that we ran.

**How we tested.** On 2026-11-08, Puppeteer 25.7 and Playwright 1.64 against a local Node server with three pages: `/redirect` (a page that sends the browser on with `location.href` 200 ms after it loads, like a login hop or a locale switch), `/form` (a form with a submit button) and `/final` (the destination, answering after 300 ms like a real server).

## What the error means

Every document a tab loads gets its own JavaScript world — the execution context. `page.evaluate()`, `page.$eval()`, `page.waitForFunction()` and element handles all live in one. When the tab navigates to a new document, Chrome destroys the old context and creates a new one. A call still running in the old context has nowhere to return to, and Puppeteer rejects it:

```text
Error: Execution context was destroyed, most likely because of a navigation.
```

Playwright, for the same script:

```text
page.evaluate: Execution context was destroyed, most likely because of a navigation.
```

What counts as a navigation here is a new document: a redirect by script or `<meta http-equiv="refresh">`, a form submit, a click on a normal link, `location.reload()`. We checked the other cases: `history.pushState()` and a hash change kept the context, and evaluate went on working; `location.reload()` threw the error.

## Two ways to get it

**1. The page moves on by itself.** `page.goto()` returns when the first document has loaded, but that document then sends the browser elsewhere:

```javascript
await page.goto('http://localhost:8097/redirect', { waitUntil: 'load' });
// The page navigates 200 ms later, while this is still running:
const title = await page.evaluate(async () => {
  await new Promise((r) => setTimeout(r, 1000));
  return document.title;
});
// Error: Execution context was destroyed, most likely because of a navigation.
```

On real sites this is a consent platform reloading the page after "accept", a sign-in check that redirects, a geo or language redirect done in JavaScript, or a bot check that passes and loads the real page.

**2. Your own click navigates.** You click a submit button or a link and run the next step at once:

```javascript
await page.goto('http://localhost:8097/form');
await page.click('#go');              // submits the form
await page.evaluate(...);             // runs in the old page, then it goes
// Error: Execution context was destroyed, most likely because of a navigation.
```

This one is a race. With our server answering instantly, the same script passed: the new page was already there when evaluate started. With the server taking 300 ms, it failed in 8 of 10 runs. That is why the error often shows up in CI or against production and rarely on localhost.

A quieter cousin: a screenshot taken during the redirect does not fail at all. After `goto` on `/redirect`, `page.screenshot()` saved the "Loading…" page and the title read `first` — a wrong picture with no error.

## Four fixes

Each was run against the same pages and returned the final page's title.

**Fix 1: wait for the navigation you trigger, starting before the click.**

```javascript
await Promise.all([
  page.waitForNavigation(),
  page.click('#go'),
]);
const title = await page.evaluate(() => document.title); // "final"
```

The order matters: `waitForNavigation()` must be listening before the click; started after it, a fast navigation may already be over, and the wait then sits until its timeout. Puppeteer's [docs](https://pptr.dev/api/puppeteer.page.waitfornavigation) show the same `Promise.all` pattern: it "is useful when you run code that will indirectly cause the page to navigate."

**Fix 2: wait for the page's own redirect.**

```javascript
await page.goto('http://localhost:8097/redirect', { waitUntil: 'load' });
await page.waitForNavigation({ waitUntil: 'load' });
```

Use it only when you know the redirect always happens; otherwise it waits for its full timeout.

**Fix 3: wait for something only the final page has.** The most robust: you do not need to know how many hops there are.

```javascript
await page.goto('http://localhost:8097/redirect');
await page.waitForSelector('#done');
```

`waitForSelector` survives navigations: it keeps looking in each new document until the element appears ([waitForSelector](https://pptr.dev/api/puppeteer.page.waitforselector)).

**Fix 4 (Playwright): `waitForURL` and locators.**

```javascript
await page.goto('http://localhost:8097/redirect');
await page.waitForURL('**/final');
// or let a locator wait for its element:
const text = await page.locator('#done').textContent(); // "Final page"
```

Playwright's [navigation guide](https://playwright.dev/docs/navigations): "Clicking an element could trigger multiple navigations. In these cases, it is recommended to explicitly page.waitForURL() to a specific url."

What not to do: wrapping evaluate in `try/catch` and retrying hides the race without fixing it, and a fixed sleep (`setTimeout(3000)`) is slower when the site is fast and still fails when it is slow. A retry is fine as a last guard around the whole step, once the step waits for the right thing.

## Checklist when you see the error

1. Log `page.url()` just before the failing call and in the catch block. A different URL means a navigation happened in between.
2. Find what navigated: a click in your script (fix 1), a redirect in the page (fix 2 or 3), a consent banner or a login (fix 3, and see [cookie banners in screenshots](https://shareglint.com/en/guides/hide-cookie-banners-in-screenshots)).
3. Replace waits on time with waits on state: an element, a URL, a response. [Waiting for page load in Puppeteer](https://shareglint.com/en/guides/puppeteer-wait-for-page-load) measures the `waitUntil` options side by side.
4. Keep element handles short-lived. A handle taken before a navigation points into the destroyed context; query again after it.

## When the job is just a screenshot

If all your script does is open a URL and save a picture, this whole class of problems is something our screenshot API handles for you: before it captures, it waits for any navigation that a click, a meta refresh or a script has started to land, and it never photographs a refused or failed document. Consent platforms that reload after "accept" are waited for the same way.

```bash
curl -X POST "https://shareglint.com/v1/screenshots?wait=true" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/", "wait_for": "#content", "block_cookie_banners": true}'
```

`wait_for` waits for a selector of the page you want, like fix 3. A viewport screenshot is 1 render of the quota, a full page 2 ([pricing](https://shareglint.com/en/pricing)). For pages behind a login or multi-step flows, keep your own Puppeteer or Playwright: the API only takes public URLs. More: [Puppeteer screenshots](https://shareglint.com/en/guides/puppeteer-screenshot), [Playwright screenshots](https://shareglint.com/en/guides/playwright-screenshot).

## FAQ

### What does "Execution context was destroyed, most likely because of a navigation" mean?

Your script was running JavaScript in a page (page.evaluate, page.$eval, a waitForFunction) when the browser left that page for a new document: a redirect, a form submit, a reload. The old document's JavaScript world is thrown away, and the call that was running in it fails with this error.

### How do I fix execution context was destroyed in Puppeteer?

Wait for the navigation before you evaluate. If you trigger it with a click, start waiting first: await Promise.all([page.waitForNavigation(), page.click(selector)]). If the page sends you on by itself, wait for the final page: page.waitForNavigation() or page.waitForSelector() of an element only the final page has.

### Does history.pushState cause this error?

No. In our test, pushState and a hash change kept the same document and evaluate went on working; location.reload() threw the error. Single-page apps that route with the History API do not destroy the context, a full page load does.

### Does Playwright have the same error?

Yes, with the same wording: "page.evaluate: Execution context was destroyed, most likely because of a navigation." Playwright's locators wait for their element and retry, so code written with locators and page.waitForURL() rarely meets it; raw page.evaluate calls still can.
