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

  1. Serialize close vs. usage: await all pending operations before calling context/browser.close(), and don't use handles after close.
  2. Detect the condition with isTargetClosedError(e) (playwright-core errors) in your catch block and treat it as end-of-session rather than retrying blindly.
  3. For browser crashes, check logs/exit code and relaunch a fresh browser instead of reusing the closed one.
  4. 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

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


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/aaef1080e180618b. Report an issue: GitHub.