In a Playwright teardown, when do you call page.close(), context.close() and browser.close()?
answer
- Three closes, three nested scopes
- A tab going away leaves the context
- Graceful shutdown happens at the context
- Artifacts are flushed on context close
- Browser close is closer to force-quitting
basics
~10 spage.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 sThe 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 linesimport { 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
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.
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.
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.
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