microsoft/playwright · error · ProtocolError
closed
closed
Error message
Target page, context or browser has been closed
What it means
WKConnection.send() refuses to dispatch a protocol command when the session is in a terminal state: _crashed, _disposed, or the browser-level disconnected logs are set. It throws a ProtocolError with code 'closed' (or 'crashed'), carrying the browser-disconnect log payload so callers see why the target died.
Source
Thrown at packages/playwright-core/src/server/webkit/wkConnection.ts:125
private _disposed = false;
private readonly _rawSend: (message: any) => void;
private readonly _callbacks = new Map<number, { resolve: (o: any) => void, reject: (e: ProtocolError) => void, error: ProtocolError }>();
private _crashed: boolean = false;
constructor(connection: WKConnection, sessionId: string, rawSend: (message: any) => void) {
super();
this.setMaxListeners(0);
this.connection = connection;
this.sessionId = sessionId;
this._rawSend = rawSend;
}
async send<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
if (this._crashed || this._disposed || this.connection._browserDisconnectedLogs)
throw new ProtocolError(this._crashed ? 'crashed' : 'closed', undefined, this.connection._browserDisconnectedLogs);
const id = this.connection.nextMessageId();
const messageObj = { id, method, params };
this._rawSend(messageObj);
return new Promise<Protocol.CommandReturnValues[T]>((resolve, reject) => {
this._callbacks.set(id, { resolve, reject, error: new ProtocolError('error', method) });
});
}
sendMayFail<T extends keyof Protocol.CommandParameters>(method: T, params?: Protocol.CommandParameters[T]): Promise<Protocol.CommandReturnValues[T] | void> {
return this.send(method, params).catch(error => debugLogger.log('error', error));
}
markAsCrashed() {
this._crashed = true;
}
isDisposed(): boolean {
return this._disposed;View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Ensure all page/context work is awaited before calling close(); structure teardown with Promise.all over in-flight ops first.
- Guard sends with page.isClosed()/context.browser() checks, or wrap in try/catch and ignore 'closed' codes during teardown.
- Investigate the actual crash: this error is a symptom; the disconnect logs in the error payload tell you why the target died.
Example fix
// before
await page.click('#x');
await browser.close();
await page.title(); // throws closed
// after
const title = await page.title();
await browser.close(); Defensive patterns
Strategy: try-catch
Validate before calling
function canSend(page) { return !page.isClosed(); }
if (canSend(page)) await page.title(); Type guard
null
Try / catch
try { await page.title(); }
catch (e) {
if (e.name === 'TargetClosedError' || /has been closed/.test(e.message)) return; // teardown race
throw e;
} Prevention
- Await all in-flight page ops before close().
- Guard sends with page.isClosed() during teardown.
When it happens
Trigger: Any protocol send after page.close()/context.close()/browser.close() resolves, after a tab crash, or after the browser process exits. Commonly surfaces when an awaited operation races with teardown — e.g. an evaluate or screenshot completing after the test called close.
Common situations: Teardown races in afterEach hooks, operations on a page whose browser crashed, reusing a context after browser.disconnect(), or fire-and-forget code that runs past close(). On WebKit this is the unified 'target gone' signal.
Related errors
- Target page, context or browser has been closed
- Target page, context or browser has been closed
- Cannot find object to "${method}": ${guid}
- Unknown new child: ${params.guid}
- Cannot find parent object ${parentGuid} to create ${guid}
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/c859258e1d124a7f.
Report an issue: GitHub.