How to fix Playwright "Execution context was destroyed" errors?

Playwright

"Execution context was destroyed" appears when Playwright has a JavaScript handle or evaluation in flight and the page navigates to a new document before that evaluation completes. The V8 JavaScript engine context for the old page is torn down, and any reference pointing into it becomes invalid. It most often surfaces from calls to page.evaluate(), locator.evaluate(), or ElementHandle methods that span a navigation triggered by a click or form submit.

Common mistake

test('submits order form', async ({ page }) => {
  await page.goto('https://shop.example.com/cart');

  const submitButton = await page.$('button[type="submit"]');

  await submitButton.click(); // Triggers navigation to /order-confirmation

  // submitButton handle is now in the destroyed old-page context
  const label = await submitButton.textContent();
});

Holding an ElementHandle (the result of page.$() or page.$$()) across a navigation is the primary cause. These handles are bound to a specific document context, unlike locators.

The fix

Switch from ElementHandle to locators, which are always re-resolved against the current document:

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

test('completes order and confirms', async ({ page }) => {
  await page.goto('https://shop.example.com/cart');

  // Use locator — re-queries the DOM on each use
  const submit = page.getByRole('button', { name: 'Place order' });
  await expect(submit).toBeEnabled();
  await submit.click();

  // Wait for the new document before asserting
  await page.waitForURL('**/order-confirmation');
  await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
});

When you need to evaluate JavaScript after a navigation, always wait for the new page state first:

await Promise.all([
  page.waitForURL('**/dashboard'),
  page.getByRole('button', { name: 'Sign in' }).click(),
]);

// Safe to evaluate — we are now in the new document's context
const userId = await page.evaluate(() => window.__USER_ID__);

Why it works

Locators in Playwright are lazy — they describe how to find elements but don't hold a live reference until an action is called. Each action re-queries the DOM in the current document's execution context, so a navigation between action and re-query doesn't cause a stale-context error. ElementHandle, by contrast, is an eager reference tied to a specific document context and becomes invalid the moment that document is replaced. Waiting for the new URL ensures the execution context has switched to the new document before any evaluation.

Tips

  • Avoid page.$() and page.$$() entirely in new test code — they return ElementHandle which is the primary source of this error. Use page.locator() and its semantic variants instead.
  • If you must use page.evaluate(), call it only after explicitly waiting for the expected navigation destination.
  • SPAs that use client-side routing don't always trigger a "real" navigation but may still replace DOM nodes, causing stale handles — locators handle this gracefully.
  • The root cause is the same as covered in execution context was destroyed (navigation) — that page focuses on Promise.all coordination patterns.