microsoft/playwright · warning · Error
Session already detached. Most likely the page has been clos
Error message
Session already detached. Most likely the page has been closed.
What it means
Thrown by CRConnection.detach() (crConnection.ts:178) when this._closed is already true. _closed is flipped to true by dispose(), which runs when the owning page/frame target is destroyed or when detach ran once before. The message tells the user the page has most likely been closed under them.
Source
Thrown at packages/playwright-core/src/server/chromium/crConnection.ts:178
callback.reject(callback.error);
} else {
callback.resolve(object.result);
}
} else if (object.id && object.error?.code === -32001) {
// Message to a closed session, just ignore it.
} else {
assert(!object.id, object?.error?.message || undefined);
Promise.resolve().then(() => {
if (this._eventListener)
this._eventListener(object.method!, object.params);
(this.emit as any)(object.method as any, object.params);
});
}
}
async detach() {
if (this._closed)
throw new Error(`Session already detached. Most likely the page has been closed.`);
if (!this._parentSession)
throw new Error('Root session cannot be closed');
// Ideally, detaching should resume any target, but there is a bug in the backend,
// so we must Runtime.runIfWaitingForDebugger first.
await this._sendMayFail('Runtime.runIfWaitingForDebugger');
await this._parentSession.send('Target.detachFromTarget', { sessionId: this._sessionId });
this.dispose();
}
dispose() {
this._closed = true;
this._connection._sessions.delete(this._sessionId);
this._rejectPendingCallbacks(`Internal server error, session closed.`);
}
private _rejectPendingCallbacks(message: string) {
for (const callback of this._callbacks.values()) {
callback.error.setMessage(message);View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Wrap detach() in try/catch and ignore the 'Session already detached' error during teardown.
- Track detachment in your own flag and skip the call when already closed.
- Detach in a page.close listener rather than relying on out-of-band cleanup.
Example fix
// before
await session.detach();
// after
try { await session.detach(); } catch (e) {
if (!/Session already detached/.test(String(e))) throw e;
} Defensive patterns
Strategy: try-catch
Try / catch
try {
await session.detach();
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if (!msg.includes('Session already detached')) throw e;
// expected during teardown
} Prevention
- Detach in a page.on('close') listener rather than in finally blocks.
- Keep a local boolean (detached) and set it after detach() and in the close handler.
- Never call detach() twice; design teardown as idempotent.
When it happens
Trigger: Calling cdpSession.detach() twice; calling detach() in a finally block after page.close() already disposed the session; the page crashed or was closed by the app while cleanup was running.
Common situations: try/finally teardown that detaches unconditionally; reused sessions across navigations that recreate targets; tests that close the page in afterEach while the test body also detached.
Related errors
- Root session cannot be closed
- Could not connect to ${channel}.\n${remoteDebuggingHint(chan
- Playwright manages remote debugging connection itself.
- Connecting to ${channel} by channel name is not supported on
- Could not connect to ${channel}: DevToolsActivePort file not
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/63e727c25fffde9f.
Report an issue: GitHub.