puppeteer/puppeteer · error · TargetCloseError

Protocol error (${method}): Session closed. Most likely the

Error message

Protocol error (${method}): Session closed. Most likely the page has been closed.

What it means

BidiCdpSession.send() throws TargetCloseError (a ProtocolError subclass) when the session's #detached flag is true, i.e. the underlying target/page was closed but the caller still holds the CDPSession reference and tried to send a command. The message embeds the method name being sent.

Source

Thrown at packages/puppeteer-core/src/bidi/CDPSession.ts:78

    return undefined;
  }

  override get detached(): boolean {
    return this.#detached;
  }

  override async send<T extends keyof ProtocolMapping.Commands>(
    method: T,
    params?: ProtocolMapping.Commands[T]['paramsType'][0],
    options?: CommandOptions,
  ): Promise<ProtocolMapping.Commands[T]['returnType']> {
    if (this.#connection === undefined) {
      throw new UnsupportedOperation(
        'CDP support is required for this feature. The current browser does not support CDP.',
      );
    }
    if (this.#detached) {
      throw new TargetCloseError(
        `Protocol error (${method}): Session closed. Most likely the page has been closed.`,
      );
    }
    const session = await this.#sessionId.valueOrThrow();
    const {result} = await this.#connection.send(
      'goog:cdp.sendCommand',
      {
        method: method,
        params: params,
        session,
      },
      options?.timeout,
    );
    return result.result;
  }

  override async detach(): Promise<void> {
    if (

View on GitHub (pinned to d484e21c17)

Solutions

  1. Check session.detached before sending, or await page-close before issuing further commands.
  2. Discard CDPSession references in the same finally block that closes the page.
  3. Catch TargetCloseError around long-running CDP calls and treat it as a graceful shutdown.

Example fix

// before
await page.close();
await session.send('Page.navigate', { url }); // throws
// after
await page.close();
if (!session.detached) {
  await session.send('Page.navigate', { url });
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (session.detached) throw new Error('Session already detached');

Type guard

const isAlive = (s: CDPSession) => !s.detached;

Try / catch

try { return await session.send(method, params); } catch (e) { if (e instanceof TargetCloseError) { /* target gone; clean up */ return; } throw e; }

Prevention

When it happens

Trigger: Calling session.send(...) after page.close() / target.destroy() / browser.disconnect() has fired the session's onClose (which sets #detached = true).

Common situations: Forgetting to drop a CDPSession reference after closing the page; an async operation that outlives the page (e.g. a long Performance.getMetrics call issued just before page.close()); a navigation that tears down the target.

Related errors


AI-assisted analysis of puppeteer/puppeteer@d484e21c17 (2026-08-12). Data as JSON: /api/errors/3873d16e20e82632. Report an issue: GitHub.