How to fix "Execution context was destroyed, most likely because of a navigation"?

Playwright

The message "execution context was destroyed, most likely because of a navigation" means Playwright started evaluating JavaScript in a document that was replaced before the evaluation completed. It appears most often as page.evaluate: execution context was destroyed, most likely because of a navigation when a click triggers a full-page navigation and your script tries to read from the old document immediately after. The same error shows up when using stale ElementHandle references or page.$eval() calls that span a navigation boundary.

Common mistake

await page.getByRole('link', { name: 'Next page' }).click();

// The click triggered a navigation — the old context is already gone
const title = await page.evaluate(() => document.title);

The click() returns before the navigation completes, so the evaluate call lands in a torn-down document.

The fix

Wrap the click and navigation wait together so Playwright knows to hold for the new document before evaluating:

import { test, expect } from '@playwright/test';

test('reads title after navigation', async ({ page }) => {
  await page.goto('https://example.com/articles');

  await Promise.all([
    page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
    page.getByRole('link', { name: 'Next page' }).click(),
  ]);

  // Safe — new document is fully committed
  const title = await page.evaluate(() => document.title);
  expect(title).toContain('Page 2');
});

The modern Playwright equivalent using waitForURL is often cleaner:

await page.getByRole('link', { name: 'Next page' }).click();
await page.waitForURL('**/articles?page=2');

// Locator-based assertions auto-retry in the new document
await expect(page.getByRole('heading', { name: /Page 2/ })).toBeVisible();

For SPAs that update the URL without a full reload, waitForURL is usually sufficient. For multi-page apps, waitForNavigation with commit or domcontentloaded is more reliable than load.

Why it works

When a navigation occurs, Chromium destroys the V8 execution context for the departing document. Any pending evaluate call that was holding a reference to that context receives the "execution context was destroyed" error. Using Promise.all with waitForNavigation ensures your script only proceeds after Playwright has attached to the new document's context. Locator-based assertions avoid the problem entirely because they re-query the DOM on each retry in the current document.

Tips

  • Prefer locator assertions (expect(locator).toBeVisible()) over page.evaluate() where possible — they are context-safe across navigations.
  • Avoid storing ElementHandle references across navigation boundaries; always re-query after the new page has loaded.
  • If evaluation must happen immediately after a navigation-triggering action, waitForURL is simpler than waitForNavigation for most cases and accepts URL patterns or predicates.
  • Related error with a similar root cause: element not attached to the DOM often follows the same navigation race pattern but surfaces at the locator action level.