Playwright: Target Page, Context or Browser Has Been Closed

9 min readupdated September 10, 2026

Your test passes, or seems to, and then this appears:

Error: locator.click: Target page, context or browser has been closed

Nothing in your test closes anything. You never call browser.close(). The message looks like a Playwright bug.

It is not. In almost every case this means a missing await.

What the error really says

Playwright tears down the browser context when a test finishes. That is normal and it is how you get isolation between tests.

The error appears when an action is still running at that moment. The test function returned, Playwright cleaned up, and then a leftover operation tried to touch a page that no longer exists.

So read it as: something was still in flight when the test ended. The question is what.

Cause 1: A missing await

This is the answer most of the time.

test('saves the contact', async ({ page }) => {
  await page.goto('/contacts');
  page.getByRole('button', { name: 'Save' }).click(); // no await
  await expect(page.getByTestId('toast')).toBeVisible();
});

The click returns a promise nobody waits for. The test may finish while the click is still resolving. Playwright disposes the context, the click lands on nothing, and you get the closed-target error, often pointing at a line that looks fine.

That last part is what makes this so confusing: the error usually surfaces somewhere other than the bug. The reported line is where the fallout landed, not where the missing await is.

The fix is to await everything, including the calls that look fire-and-forget.

Cause 2: A cleanup step that is not awaited

The same bug, in the place people least expect it.

test.afterEach(async ({ request }) => {
  request.delete('/api/test-data'); // no await
});

The hook returns immediately. Playwright tears down. The request rejects afterwards against a disposed context.

Anything in afterEach, afterAll or a finally block needs await exactly as much as the test body does.

Cause 3: A promise you started and never joined

// Wrong: the waiter is created after the click that triggers it
await page.getByRole('link', { name: 'Report' }).click();
const download = await page.waitForEvent('download');

The correct pattern starts the waiter first, then triggers, then awaits both:

const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'Report' }).click();
const download = await downloadPromise;

If you start a waiter and never await it, it is still pending when the test ends, and it rejects into the same error.

Cause 4: The test timed out

When a test hits its timeout, Playwright stops it and disposes the context. Any action still running reports the closed target.

Here the closed-target message is a symptom, not the cause. Look above it in the output for the timeout. If a test times out at 30 seconds and then reports a closed target, fix the timeout, not the close.

Timeout 30000ms exceeded covers that side.

Cause 5: Reusing a page after its context is gone

If you manage the browser yourself rather than using the page fixture, the ordering is yours to get right:

const context = await browser.newContext();
const page = await context.newPage();
await context.close();
await page.click('#submit'); // the context is gone

Storing a page in a variable outside a test, and using it in the next one, produces the same thing. Each test gets a fresh context, so a page from a previous test is already dead.

How to find the missing await in five minutes

The error will not point at the bug, so do not start from the reported line.

1. Turn on the lint rule. This is the highest-value step and it catches the whole class of bug before you run anything. With TypeScript, enable @typescript-eslint/no-floating-promises. It flags every promise you did not await.

2. Search for the usual suspects. Look for lines that call an action without await:

grep -rnE "^s+(page|locator).[a-zA-Z]+(" tests/ | grep -v await

3. Check every hook and finally block. They are the most commonly missed.

4. Read upward from the failure. If there is a timeout above it, that is the real error.

I have watched someone spend most of a day on this, convinced Playwright had a race condition, because the error pointed at line 40 and the missing await was on line 12. The lint rule found it in seconds.

Why the message is so unhelpful

Playwright reports where the failure surfaced, because that is the only place it can observe it. By the time a floating promise rejects, the stack that created it is long gone.

This is exactly why the lint rule matters more than any debugging technique here. The bug is structural and a linter can see it. A stack trace cannot.

Quick reference

Symptom Likely cause Fix
Fails intermittently, different lines Missing await on an action Await everything, enable the lint rule
Fails in afterEach or afterAll Un-awaited cleanup Await the hook body
Fails right after a timeout The timeout is the real error Fix the timeout
Fails on a download or popup Waiter created after the trigger Create the promise first, await after
Fails on the first action of a test A page kept from a previous test Use the page fixture

Common mistakes

  1. Debugging the reported line. It is where the fallout landed, not where the bug is.
  2. Adding a sleep before the end of the test. It hides the race and it will come back in CI.
  3. Wrapping the action in try/catch. That swallows a real bug and leaves a test that proves nothing.
  4. Assuming it is a Playwright bug. It is possible, and it is not where to start.

Frequently asked questions

Why does it only fail sometimes? Because it is a race. Whether the leftover action finishes before teardown depends on machine speed, which is why it shows up in CI first.

Why does the error point at the wrong line? A floating promise rejects long after the code that created it has left the stack. Playwright can only report where it noticed.

Does this happen with the page fixture too? Yes. The fixture manages the context lifecycle correctly, but it cannot await a promise you never awaited.

Is there a way to prevent it entirely? Close to it. Enable @typescript-eslint/no-floating-promises and it will catch nearly every instance before you run the test.

Remember this

The message is about timing, not about the browser. Something was still running when the test ended.

Nine times out of ten it is a missing await, often in a hook or a cleanup block, and usually nowhere near the line in the error. Turn on the floating-promises lint rule and this class of bug mostly stops happening.

The Playwright tutorial covers why every action needs an await in the first place.

Put it into practice

Solve a graded problem: write a test, and we check it would catch a real bug.

Browse the problems →

Learn this in full

Reading about a problem is not the same as fixing one. Pick a lecture or a practice problem and write the test yourself.

Keep reading