When a Playwright test fails, most engineers immediately scroll to the stack trace line number and read the wrong part first. The call log in the middle of the error message is where the actual diagnosis is: it shows what condition Playwright was polling for and what it observed, telling you whether the element was missing, hidden, disabled, or covered by an overlay. This guide explains how to read all three parts of a Playwright error, what the most common error types mean, and the four-step debugging workflow that resolves most failures.
The anatomy of a Playwright error
Error: locator.click: Timeout 30000ms exceeded.
Call log:
- waiting for getByRole('button', { name: 'Submit' })
waiting for element to be visible, enabled and stable
element is not visible - waiting...
element is not visible - waiting...
element is not visible - waiting...
at Object.<anonymous> (tests/checkout.spec.ts:45:40)Three parts:
1. The error type and message: locator.click: Timeout 30000ms exceeded — action failed, what timed out
2. Call log: What Playwright was doing and observing while waiting
3. Stack trace: Where in your test code it happened
Read them in that order: error type first, call log second, stack trace third. The call log tells you the most.
Common errors and what they mean
Timeout exceeded
Error: locator.click: Timeout 30000ms exceeded.Playwright couldn't complete the action within the timeout. The call log tells you why. Common causes:
- Element doesn't exist in the DOM
- Element exists but is hidden (
display: none,visibility: hidden,opacity: 0) - Element exists and is visible but is disabled
- Element is covered by another element (overlay, modal, toast notification)
- Page navigation didn't complete before the next action
element is not visible, element is not stable, element is disabled). That's your actual problem.
Strict mode violation
Error: strict mode violation: getByRole('button', { name: 'OK' }) resolved to 2 elementsYour locator matched more than one element. Playwright requires locators to be unique unless you explicitly say otherwise.
Fix: Be more specific. Add a context filter:// Too broad
page.getByRole('button', { name: 'OK' })
// More specific — scoped to the modal
page.getByRole('dialog').getByRole('button', { name: 'OK' })
// Or use nth if ordering is stable
page.getByRole('button', { name: 'OK' }).first()Locator not found
Error: locator.fill: Error: strict mode violation: getByLabel('Email') resolved to 0 elementsZero elements matched. Either the locator is wrong, the element isn't on the page yet, or you're on the wrong page.
Fix: Check the locator. Open the test with--debug flag to inspect the page state:
npx playwright test --debug tests/login.spec.tsNavigation timeout
Error: page.goto: Timeout 30000ms exceeded.
call log:
navigating to "http://localhost:3000/login", waiting until "load"The page didn't finish loading. Either:
- The server is slow or not running
- The
waitUntiloption is set too strict ('networkidle'on a page with background polling) - A resource (image, script) is hanging the load event
waitUntil: 'domcontentloaded' if you don't need to wait for all resources. Use the Network tab in Trace Viewer to see what's hanging.
Expect timeout
Error: expect(locator).toHaveText() with timeout 5000ms
Received string: ""
Expected string: "Order confirmed"The assertion didn't pass within the timeout. The element exists but has the wrong content (or no content yet).
Fix: Is the content actually loading? Add a longer timeout temporarily to rule out a timing issue:await expect(el).toHaveText('Order confirmed', { timeout: 15000 }). If it passes with more time, you have a performance issue, not a selector issue.
Execution context destroyed
Error: Execution context was destroyed, most likely because of a navigation.Playwright was holding a reference to a page element when the page navigated away. A common cause: you did const text = page.getByText('foo') and then navigated before evaluating it.
// Broken — locator evaluated after navigation
const text = page.getByText('Confirmation');
await page.getByRole('button', { name: 'Submit' }).click(); // triggers navigation
await expect(text).toBeVisible(); // page is gone
// Fixed — assert before navigation triggers, or re-query after navigation
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page).toHaveURL('/confirmation');
await expect(page.getByText('Order confirmed')).toBeVisible();Target closed
Error: Target page, context or browser has been closedThe browser context was closed before the test finished. Usually happens when:
- A test closes the context manually in the wrong place
- A
beforeAllblock closed a context thatafterAllor tests still need - A test crashed and cleanup code ran while other things were still running
Network errors
Error: net::ERR_CONNECTION_REFUSED at http://localhost:3000The server isn't running. Check:
- Is your dev server started before tests?
- Is the
baseURLcorrect? - Is the port correct?
In CI, add a step to start your server before Playwright runs, or use webServer in config:
// playwright.config.ts
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},Debugging workflow
Step 1: Read the error type and call log. The call log tells you what Playwright was waiting for and what it observed. Step 2: Open Trace Viewer.npx playwright show-trace test-results/.../trace.zip: the DOM snapshot shows you exactly what was on the page when the failure happened.
Step 3: Run in debug mode (--debug) to step through the test interactively.
Step 4: Use page.pause() to freeze the test at a specific point and inspect manually.
await page.goto('/checkout');
await page.pause(); // Test freezes here, opens Inspector
await page.getByRole('button', { name: 'Pay' }).click();The Inspector opens a browser with the Playwright toolbar — you can interact with the page, run locator queries, and watch actions in real time.
→ See also: Playwright Trace Viewer: Debug Failing Tests Like a Pro | Debugging Flaky Tests: A Practical Guide | Playwright Assertions: The Complete Guide