skip to content

In a Playwright teardown, when do you call page.close(), context.close() and browser.close()?

level: seniorimportance: should knowfreq 47%

answer

  1. Three closes, three nested scopes
  2. A tab going away leaves the context
  3. Graceful shutdown happens at the context
  4. Artifacts are flushed on context close
  5. Browser close is closer to force-quitting

basics

~10 s

page.close() ends one tab and leaves its context running. context.close() closes the context and all its pages, flushing artifacts. browser.close() ends everything at once, like force-quitting, so close your contexts before it.

solid answer

~40 s

The three calls are nested. `page.close()` closes one tab; its context and sibling pages survive, and closing the last page does *not* close the context. `context.close()` closes the context and every page in it, shutting each down gracefully so page `close` events fire and context-scoped artifacts such as recorded video and HAR are flushed to disk. `browser.close()` ends the browser and all its contexts — Playwright's docs liken it to force-quitting, which is why you should `await context.close()` on contexts you created before calling it. Useful options: `page.close({ runBeforeUnload: true })` runs `beforeunload` handlers and returns without waiting for the close, and `{ reason }` (Playwright 1.40 onward, current in 1.63) labels the errors that interrupted operations report. `page.isClosed()` and `context.isClosed()` guard stale references.

code

typescript · 16 lines
typescript
import { chromium } from 'playwright';

const browser = await chromium.launch();
const context = await browser.newContext({ recordVideo: { dir: 'videos/' } });

try {
  const searchTab = await context.newPage();
  await searchTab.goto('https://hotels.example.com/search?city=lisbon');

  const roomTab = await context.newPage();
  await roomTab.goto('https://hotels.example.com/rooms/deluxe-suite');
  await roomTab.close({ reason: 'room comparison finished' });
} finally {
  await context.close(); // closes remaining pages and flushes the video
  await browser.close();
}

go deeper

for a junior

Learn the nesting: a page is a tab, a context is a session of tabs, a browser holds contexts, and each close takes everything below it.

for a middle

Explain that closing the last page leaves the context open, that page.close() skips beforeunload by default, and what the runBeforeUnload and reason options change.

for a senior

Demonstrate the operational consequence: contexts must be closed before the browser or graceful shutdown is skipped and video and HAR artifacts are lost, and teardown belongs in a finally so failures do not leak processes.

for a principal

Own the lifetime policy across the suite. Where contexts are created and closed determines artifact fidelity, memory footprint under parallel workers, and how cleanly a cancelled run releases browser processes in CI.

## Three closes, three scopes | Call | Closes | Leaves running | |---|---|---| | `page.close()` | that one tab | its context, and every sibling page in it | | `context.close()` | the context and all of its pages | the browser and its other contexts | | `browser.close()` | the browser and every context in it | nothing | The ladder is strictly nested, and each rung is cheap relative to the one above it. Closing a page is a tab; closing a context throws away a whole guest session; closing the browser ends the process. ## What page.close() does not do Closing the last page of a context does **not** close the context. Contexts are explicit objects with explicit lifetimes — that is the whole point of the two-level model. The single exception is a page produced by the convenience call `browser.newPage()`, which silently made a context of its own; closing that page closes that context with it. Two options are worth knowing: - `page.close({ runBeforeUnload: true })` runs the page's `beforeunload` handlers. The default is `false` — Playwright does not run them. With `true` the call returns **without waiting** for the page to actually close, and the page may summon a `beforeunload` dialog you then have to handle through the page's `dialog` event. - `page.close({ reason })`, added in Playwright 1.40 and still current in 1.63, sets the message reported to any operation interrupted by the closure. It turns a generic "target closed" error in a parallel step into "closed because the booking flow finished", which is worth minutes of debugging in a flaky run. ## What context.close() does that browser.close() does not `browser.close()` is described in Playwright's own documentation as similar to force-quitting the browser. It tears everything down, and pages do not get a graceful shutdown. `context.close()` closes each of its pages properly, so: 1. `page` close events fire and any handler you registered on them runs. 2. Artifacts that are written on context teardown are flushed to disk — recorded video and a recorded HAR are both finalised by the context closing, not by the process exiting. 3. Operations still in flight are cancelled with a clear error rather than dying with the transport. That is why the guidance is to `await context.close()` on every context you created with `browser.newContext()` **before** you call `browser.close()`, rather than relying on the browser close to sweep them up. ## Ordering a teardown 1. Close pages you opened *only* if you need the tab gone mid-test — for example to prove a second tab picked up a change after the first was closed. Routine per-page closing before a context close is redundant. 2. `await context.close()` for each context you created. This is the line that must not be skipped. 3. `await browser.close()` once, at the very end. 4. Wrap all three in a `finally` (or a fixture teardown) so a failing assertion still releases the browser process — a leaked browser is a hung CI job, not a failed one. ## Checking state before acting - `page.isClosed()` returns synchronously and tells you whether that tab is gone. - `context.isClosed()`, added in Playwright 1.59, is true while the context is closing as well as after it has closed. - Both matter inside long-lived listeners. A `context.on('page', ...)` handler on a hotel-booking suite can easily still be holding a popup that the test already finished with; touching it without a guard rejects with a target-closed error. ## The shape that goes wrong in practice A suite creates a context per test but only closes the browser in a global teardown. Every test passes. Then videos are empty or truncated, the HAR is missing its last entries, and memory climbs across a long run because dozens of contexts stayed open. Nothing failed, so nothing pointed at the cause. The fix is one `await context.close()` per created context, in teardown, before the browser goes.

  • What actually breaks if a suite only ever calls browser.close()?
    Pages are torn down abruptly, so page `close` handlers may not run and context-scoped artifacts can be lost — recorded videos empty or truncated, a recorded HAR missing its last entries. Nothing fails loudly, and contexts accumulate across a long run.
  • When would you pass runBeforeUnload: true to page.close()?
    Only when the test needs the page's `beforeunload` handlers to run, such as verifying an unsaved-booking warning. It returns without waiting for the close and can raise a `beforeunload` dialog you must handle through the page's `dialog` event, so the default false is right almost everywhere.
  • Why does closing the last page leave the context alive?
    Contexts are explicit objects with lifetimes you own — that separation is what lets a session outlive any individual tab. The one exception is a page from browser.newPage(), which created a context implicitly, so closing that page closes that context too.

saying these in an interview costs you the question

  • Expecting the context to close with its last page
  • Relying on browser.close() to flush videos and HARs
  • Closing every page by hand before closing the context
  • Assuming page.close() runs beforeunload by default
  • Skipping teardown on failure and leaking browser processes