ScreenshotNeo

BlogHow-to

How to Fix “Execution Context Was Destroyed” in Puppeteer

Learn why Puppeteer loses an execution context during navigation and how to wait for the right page state, reacquire elements, and troubleshoot failures.

By the ScreenshotNeo team4 October 20268 min read

Direct answer: Puppeteer throws “Execution context was destroyed” when an evaluation or element operation is interrupted because the JavaScript context for that page or frame has been disposed, commonly during navigation. If your action should navigate, start page.waitForNavigation() before the click or submit and await both together. If navigation is optional or the app updates in place, wait for a selector or application state that proves the next step is ready, then query the page again.

The error identifies a context lifecycle race; by itself, it does not prove the browser crashed, Puppeteer is defective, or CI is the cause. Find the operation that failed and what the page or frame was doing immediately before it.

1. Understand what the error means

Puppeteer evaluates JavaScript in an execution context associated with a page or frame. When navigation replaces a document, its old context is disposed. If an evaluation is still trying to use that context, it can fail before a replacement context is available. Puppeteer’s current CDP implementation tracks context disposal and waits for a context when an evaluation begins without one; disposal before a replacement arrives produces this error. See the Puppeteer implementation.

Common triggers include clicking a link, submitting a form, calling page.goto() or page.reload(), following a redirect, using browser history, or losing a frame. It can also happen when code starts a navigation but does not await it before querying or evaluating. Site timing matters: a reload-and-selector sequence was reported to fail on one site and not another, so an individual report is not evidence of a universal browser bug. Puppeteer issue #10435.

2. Fix an action that is expected to navigate

Register the navigation wait before triggering the action. This ordering matters because a fast navigation could start before a later wait is registered.

const [response] = await Promise.all([
  page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
  page.click('a#navigate-away'),
]);

console.log('Destination:', page.url());
console.log('Navigation response:', response?.status() ?? 'no main-resource response');
const title = await page.title();
console.log('Title:', title);

The array starts both operations before Promise.all waits for them. Choose a lifecycle condition based on what the next step needs. domcontentloaded is often enough to query parsed markup; it does not mean every image or later application request has finished. You can use another supported waitUntil lifecycle condition when the workflow needs it. Do not default to network idle without checking the site: persistent requests can make it an unsuitable readiness signal. The recommended wait-before-action pattern and readiness tradeoffs are described in this Puppeteer error guide.

For a form submission, use the same pattern:

await Promise.all([
  page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
  page.click('form button[type="submit"]'),
]);

const heading = await page.$eval('h1', element => element.textContent?.trim());
console.log(heading);

Replace the selectors with ones that match your page. If the form is handled by client-side JavaScript and does not navigate, use the in-place update pattern in the next section instead.

3. Wait for the result when navigation is uncertain

Some clicks update a single-page application, show a modal, or change content without replacing the document. In those cases, a navigation wait can time out or fail to describe readiness. Wait for a state-specific signal that means the result you need is present:

await page.click('button#submit');
await page.waitForSelector('.success-message', { visible: true });

const message = await page.$eval(
  '.success-message',
  element => element.textContent?.trim()
);
console.log(message);

Choose a selector that distinguishes the completed state. A generic heading is only a useful signal if it reliably changes to the intended result. When no stable selector exists, wait for an application-specific condition that can be observed from the page, and use a finite timeout so a failed transition is reported clearly.

4. Handle reloads, redirects, and frames safely

Reloads and redirects

Wait for the reload or navigation before using the new document:

await Promise.all([
  page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
  page.reload(),
]);

await page.waitForSelector('[data-page-ready="true"]');
const result = await page.$eval('h1', element => element.textContent?.trim());
console.log(result);

For a redirect caused by a click or submit, wait for the navigation initiated by that action, then inspect page.url() and query the destination. A navigation response can be absent for some navigation types, so do not assume the response value is always non-null.

Frames

Frame navigation or detachment can invalidate work performed against that frame. Identify the relevant frame after the transition and reacquire its elements. Do not assume a handle from a frame or document that has navigated is still usable.

Element handles

Element handles belong to the document context in which they were obtained. Re-query after navigation instead of carrying a handle across the transition:

// Navigate first, then acquire an element from the new document.
await Promise.all([
  page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
  page.click('a#next'),
]);

const newPageLink = await page.$('a#continue');
if (!newPageLink) throw new Error('Continue link not found on destination page');
await newPageLink.click();

5. A complete runnable example

This Node.js example opens a page, clicks a link that is expected to navigate, waits for the new document, reacquires the destination heading, and reports useful context if an operation fails. Install Puppeteer in your project with npm install puppeteer, save this as capture.js, then run node capture.js. Set START_URL and NAVIGATION_SELECTOR to a page and link you control.

const puppeteer = require('puppeteer');

async function main() {
  const startUrl = process.env.START_URL;
  const navigationSelector = process.env.NAVIGATION_SELECTOR;
  if (!startUrl || !navigationSelector) {
    throw new Error('Set START_URL and NAVIGATION_SELECTOR');
  }

  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  page.setDefaultNavigationTimeout(30000);
  page.setDefaultTimeout(10000);

  try {
    await page.goto(startUrl, { waitUntil: 'domcontentloaded' });
    const previousUrl = page.url();

    await Promise.all([
      page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
      page.click(navigationSelector),
    ]);

    await page.waitForSelector('h1');
    const heading = await page.$eval('h1', element => element.textContent?.trim());
    console.log({ previousUrl, currentUrl: page.url(), heading });
  } catch (error) {
    console.error('Puppeteer operation failed:', error);
    console.error('Current URL:', page.url());
    console.error('Frames:', page.frames().map(frame => frame.url()));
    throw error;
  } finally {
    await browser.close();
  }
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

If the click sometimes navigates and sometimes updates the page in place, this example’s navigation wait is not the right universal strategy. Branch on the workflow or wait for the specific resulting state instead.

6. Troubleshooting checklist

Symptom Likely cause Fix
Error immediately after click or submit The document began navigating while a query or evaluation was still using its old context. When navigation is expected, register waitForNavigation() before the action and await both with Promise.all.
waitForNavigation() times out The action updated the current page, opened a modal, did nothing, or failed validation without a document navigation. Wait for a result-specific selector or application-state signal; check that the action actually succeeded.
Failure after reload or redirect Code reused an element handle or queried before the new document was ready. Await the transition and reacquire selectors and handles from the new document.
Only one frame fails The target frame navigated, changed, or detached while work was in progress. Log frame URLs, reacquire the active frame after the transition, and query within that frame.
Intermittent or CI-only failure Timing, runtime versions, page behavior, missing elements, or different waits may vary across environments. A CI issue report mentions multiple such factors and does not isolate a universal CI cause. Compare the exact Node.js and Puppeteer versions from the lockfile and CI image; record the URL, action, frame, selector, and timing around the failure. See Puppeteer issue #12968.
Waiting for network idle never completes The site keeps requests open or continues background traffic. Wait for the DOM lifecycle event or page-specific selector that establishes readiness for your next operation.
Increasing the timeout does not help The problem is an invalidated context or wrong readiness condition, rather than merely a slow page. Correct the action/wait ordering or choose a state signal that matches the workflow before raising timeouts.
  1. Locate the exact failing Puppeteer call and whether it targets a page, frame, or element handle.
  2. Check whether the preceding click, submit, reload, goto(), redirect, or history action navigated or detached a frame.
  3. If navigation is expected, begin the navigation wait before the action and await both.
  4. If navigation is uncertain, wait for the completed result instead of assuming a document load.
  5. Reacquire handles after the transition.
  6. If it persists, compare runtime versions and capture URL, frame, lifecycle, and timeout details.

7. Reliability, performance, and cost considerations

Reliability: Match the wait to the event that makes the next operation safe. Navigation waits are appropriate for a full document transition; state-specific waits are better for in-place updates. Keep waits finite, surface errors with the action and URL in logs, and avoid launching overlapping operations on the same page unless their ordering is deliberate.

Performance: Waiting only for the lifecycle or selector needed by the next step avoids waiting for unrelated resources. A broad network-idle wait may delay a workflow on pages with continuing traffic. Raising a timeout does not repair a context race.

Cost: The error itself has no universal cost figure. In hosted browser automation, practical costs depend on the provider and how long browser sessions and retries run; check the terms of the service you use. For a self-hosted script, account for browser runtime and infrastructure, and make retries bounded so a deterministic selector or navigation failure does not create a retry loop.

8. Or skip the browser setup

If your task is to capture a page as an image or PDF, ScreenshotNeo provides a website screenshot API and MCP server. Its one-call API can return a screenshot without you managing a Puppeteer browser for that capture. The full parameter reference is in the ScreenshotNeo documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Cookie banners are accepted and removed before the shot, along with known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Sign up for 1,000 free screenshots a month, no card required.

9. Frequently asked questions

Does this error mean Chromium crashed?

No. It indicates an operation encountered a disposed execution context. Inspect the stack trace and page transition before concluding the browser crashed.

Should I always use waitForNavigation() after a click?

No. Use it when the action is expected to trigger a document navigation. For client-side updates, wait for the resulting application state.

Can I keep an element handle after navigation?

Do not rely on it. Reacquire the element from the new document or frame after the transition.

Is this always a Puppeteer bug?

No. The message describes a context lifecycle race; reload reports also show site-dependent behavior. Diagnose the specific sequence and environment.

Will a larger timeout fix it?

Only if the page is simply taking longer than the current limit. A timeout increase cannot make a disposed context valid or substitute for waiting on the correct transition.