{"record":{"id":"63e727c25fffde9f","repo":"microsoft/playwright","slug":"session-already-detached-most-likely-the-page-has","errorCode":null,"errorMessage":"Session already detached. Most likely the page has been closed.","messagePattern":"Session already detached\\. Most likely the page has been closed\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/playwright-core/src/server/chromium/crConnection.ts","lineNumber":178,"sourceCode":"        callback.reject(callback.error);\n      } else {\n        callback.resolve(object.result);\n      }\n    } else if (object.id && object.error?.code === -32001) {\n      // Message to a closed session, just ignore it.\n    } else {\n      assert(!object.id, object?.error?.message || undefined);\n      Promise.resolve().then(() => {\n        if (this._eventListener)\n          this._eventListener(object.method!, object.params);\n        (this.emit as any)(object.method as any, object.params);\n      });\n    }\n  }\n\n  async detach() {\n    if (this._closed)\n      throw new Error(`Session already detached. Most likely the page has been closed.`);\n    if (!this._parentSession)\n      throw new Error('Root session cannot be closed');\n    // Ideally, detaching should resume any target, but there is a bug in the backend,\n    // so we must Runtime.runIfWaitingForDebugger first.\n    await this._sendMayFail('Runtime.runIfWaitingForDebugger');\n    await this._parentSession.send('Target.detachFromTarget', { sessionId: this._sessionId });\n    this.dispose();\n  }\n\n  dispose() {\n    this._closed = true;\n    this._connection._sessions.delete(this._sessionId);\n    this._rejectPendingCallbacks(`Internal server error, session closed.`);\n  }\n\n  private _rejectPendingCallbacks(message: string) {\n    for (const callback of this._callbacks.values()) {\n      callback.error.setMessage(message);","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/microsoft/playwright/blob/c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6/packages/playwright-core/src/server/chromium/crConnection.ts#L160-L196","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait session.detach();\n// after\ntry { await session.detach(); } catch (e) {\n  if (!/Session already detached/.test(String(e))) throw e;\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await session.detach();\n} catch (e) {\n  const msg = e instanceof Error ? e.message : String(e);\n  if (!msg.includes('Session already detached')) throw e;\n  // expected during teardown\n}","preventionTips":["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."],"tags":["cdp","session-lifecycle","cleanup","chromium"],"backgroundTag":null,"analyzedSha":"c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6","analyzedAt":"2026-08-12T07:26:36.950Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}