microsoft/playwright · error · TargetClosedError
Target page, context or browser has been closed
Error message
Target page, context or browser has been closed
What it means
TargetClosedError is the canonical Playwright error for 'the object you are talking to has gone away'. In dispatcher.ts:365 it is thrown at dispatch time when the target Dispatcher was disposed during the before-call instrumentation (this._dispatcherByGuid.get(guid) !== dispatcher after onBeforeCall) — i.e. the page/context/browser closed between the start of the call and the method invocation. The same class is reused for many close-related failures and its message is rewritten with the recorded closeReason when one exists.
Source
Thrown at packages/playwright-core/src/server/dispatchers/dispatcher.ts:365
endTime: 0,
type: dispatcher._type,
method,
params: params || {},
timeout: validMetadata.timeout,
log: [],
};
const beforeController = dispatcher.createProgressController(callMetadata);
this._activeProgressControllers.set(callMetadata.id, beforeController);
// Be generous with the tracing timeout in case it wants to capture a screenshot, fail silently.
await beforeController.run(progress => sdkObject.instrumentation.onBeforeCall(progress, sdkObject), 3000).catch(() => {});
this._activeProgressControllers.delete(callMetadata.id);
const response: any = { id };
try {
// If the dispatcher has been disposed while running the instrumentation call, error out.
if (this._dispatcherByGuid.get(guid) !== dispatcher)
throw new TargetClosedError(sdkObject.closeReason());
const controller = dispatcher.createProgressController(callMetadata);
this._activeProgressControllers.set(callMetadata.id, controller);
const result = await controller.run(progress => (dispatcher as any)[method](validParams, progress), validMetadata.timeout);
this._activeProgressControllers.delete(callMetadata.id);
const validator = findValidator(dispatcher._type, method, 'Result');
response.result = validator(result, '', this._validatorToWireContext());
callMetadata.result = result;
} catch (e) {
if (isTargetClosedError(e)) {
const reason = sdkObject.closeReason();
if (reason)
rewriteErrorMessage(e, reason);
} else if (isProtocolError(e)) {
if (e.type === 'closed')
e = new TargetClosedError(sdkObject.closeReason(), e.browserLogMessage());
else if (e.type === 'crashed')
rewriteErrorMessage(e, 'Target crashed ' + e.browserLogMessage());
}View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Serialize close vs. usage: await all pending operations before calling context/browser.close(), and don't use handles after close.
- Detect the condition with isTargetClosedError(e) (playwright-core errors) in your catch block and treat it as end-of-session rather than retrying blindly.
- For browser crashes, check logs/exit code and relaunch a fresh browser instead of reusing the closed one.
- Guard long pipelines by checking page.isClosed() before non-trivial actions where appropriate.
Example fix
// before: racing close with usage
context.close(); // fires concurrently
await page.click('#x'); // TargetClosedError
// after: close after work completes
await page.click('#x');
await context.close();
// and handle gracefully
catch (e) {
if (isTargetClosedError(e)) { /* session ended, do not retry the same handle */ }
else throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
// Avoid issuing calls against possibly-closed targets.
async function safeClick(page) {
if (page.isClosed?.()) throw new Error('page already closed');
await page.click('#x');
} Type guard
import { isTargetClosedError } from 'playwright-core/lib/server/errors'; // or client/errors
function isTargetClosed(e: unknown): boolean {
return e instanceof Error && (e.name === 'TargetClosedError' || /has been closed/.test(e.message));
} Try / catch
try {
await page.click('#x');
} catch (e) {
if (isTargetClosed(e)) {
// session is over: stop using this handle, optionally relaunch a fresh browser
return;
}
throw e;
} Prevention
- Await all in-flight operations before closing context/browser; never close and use concurrently.
- Check page.isClosed() before non-trivial actions in long pipelines.
- Use isTargetClosedError() (Playwright's exported helper) to distinguish close-failures from real errors instead of retrying blindly.
- On remote/browser crashes, recreate the browser rather than reusing the closed target.
When it happens
Trigger: Issuing any RPC against a Page/Frame/Context/Browser whose Dispatcher was disposed while the call was being set up: the browser crashed, context.close()/browser.close() ran concurrently, the page navigated to a scheme that destroyed it, or a prior operation tore the target down. The dispatch loop detects the disposal and raises TargetClosedError (message overwritten with sdkObject.closeReason() if available).
Common situations: Calling page.something() while another branch called context.close(); browser crashed (OOM, SIGKILL in CI); page closed by the app (window.close, download, navigation); test teardown racing with the last assertion; remote browser dropped the connection.
Related errors
- closed
- Cannot find object to "${method}": ${guid}
- Unknown new child: ${params.guid}
- Cannot find parent object ${parentGuid} to create ${guid}
- ${this._closeReason}
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/aaef1080e180618b.
Report an issue: GitHub.