microsoft/playwright · critical · Error

Browser context was closed before the daemon could start

Error message

Browser context was closed before the daemon could start

What it means

Thrown by `startCliDaemonServer` after `BrowserBackend.initialize()` completes, if the supplied `BrowserContext` is already closed. The daemon cannot serve tools against a dead context, so it aborts during startup. This is a race-condition guard, not a normal flow.

Source

Thrown at packages/playwright-core/src/tools/cli-daemon/daemon.ts:77

  }
): Promise<string> {
  const sessionConfig = createSessionConfig(clientInfo, sessionName, browserInfo, options);
  const { socketPath } = sessionConfig;

  // Clean up existing socket file on Unix
  if (process.platform !== 'win32' && await socketExists(socketPath)) {
    try {
      await fs.promises.unlink(socketPath);
    } catch (error) {
      throw error;
    }
  }

  const backend = new BrowserBackend(contextConfig, browserContext, browserTools);
  await backend.initialize(mcpClientInfo);

  if (browserContext.isClosed())
    throw new Error('Browser context was closed before the daemon could start');

  const server = net.createServer(socket => {
    const connection = new SocketConnection(socket);
    const abortController = new AbortController();
    connection.onclose = () => abortController.abort();
    connection.onmessage = async message => {
      const { id, method, params } = message;
      try {
        if (method === 'stop') {
          await deleteSessionFile(clientInfo, sessionConfig);
          const sendAck = async () => connection.send({ id, result: 'ok' }).catch(() => {});
          if (options?.exitOnClose)
            gracefullyProcessExitDoNotHang(0, () => sendAck());
          else
            await sendAck();
        } else if (method === 'run') {
          const { toolName, toolParams } = parseCliCommand(params.args);
          toolParams._meta = { cwd: params.cwd, raw: params.raw || params.json, json: !!params.json };

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Ensure the `BrowserContext` passed to `startCliDaemonServer` stays open until the server is listening (await the returned socket path before disposing).
  2. If the context is shared/attached, confirm the owning process is still alive and not itself shutting down.
  3. Move any `context.close()` / `browser.close()` calls to run only after the daemon has fully started (or stop the daemon first).

Example fix

// before
const ctx = await browser.newContext();
startCliDaemonServer('s', ctx, ...); // fire-and-forget
await ctx.close();
// after
const ctx = await browser.newContext();
await startCliDaemonServer('s', ctx, ...); // await startup
// ... use daemon ...
await ctx.close();
Defensive patterns

Strategy: try-catch

Validate before calling

// Before starting the daemon, ensure the context is alive.
function isContextUsable(ctx: any): boolean {
  return !!ctx && typeof ctx.isClosed === 'function' && !ctx.isClosed();
}

Try / catch

try {
  await startCliDaemonServer(session, ctx, info, cfg, client, mcp, opts);
} catch (e) {
  if (/Browser context was closed before the daemon could start/.test((e as Error).message)) {
    // context died during init — restart browser/context then retry, or surface to caller
  }
  throw e;
}

Prevention

When it happens

Trigger: The browser/context is closed (manually, by a SIGINT handler, by the owning process exiting, or by a parallel `stop` call) in the window between `startCliDaemonServer` being called and the `isClosed()` check at daemon.ts:77. Also reachable if `createBrowserWithInfo` returns a context that is closed before the daemon server is stood up.

Common situations: Caller tears down the context immediately after requesting daemon start; a shared/attached browser's owner process exits mid-init; tests that start a daemon on a context fixture that is already disposed; an `exitOnClose` daemon whose context is closed by a prior `stop` arriving during boot.

Related errors


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