grafana/k6 · error · websocket.CloseError

closing communication with browser: %w

Error message

closing communication with browser: %w

What it means

In Connection.send, the closeCh branch fires when a websocket close frame arrives from the browser while k6 is sending a message. k6 then runs c.close(code) and returns a *websocket.CloseError wrapped with this text, so callers see both the wrapper and the machine-readable close code.

Source

Thrown at internal/js/modules/k6/browser/common/connection.go:484

	}
	err := c.send(c.ctx, msg, nil, nil)
	if err != nil {
		c.logger.Errorf("Connection:stopWaitingForDebugger", "sid:%v wsURL:%q, err:%v", sid, c.wsURL, err)
	}
}

func (c *Connection) send(
	ctx context.Context, msg *cdproto.Message, recvCh chan *cdproto.Message, res any,
) error {
	select {
	case c.sendCh <- msg:
	case err := <-c.errorCh:
		c.logger.Debugf("Connection:send:<-c.errorCh", "wsURL:%q sid:%v, err:%v", c.wsURL, msg.SessionID, err)
		return fmt.Errorf("sending a message to browser: %w", err)
	case code := <-c.closeCh:
		c.logger.Debugf("Connection:send:<-c.closeCh", "wsURL:%q sid:%v, websocket code:%v", c.wsURL, msg.SessionID, code)
		_ = c.close(code)
		return fmt.Errorf("closing communication with browser: %w", &websocket.CloseError{Code: code})
	case <-ctx.Done():
		c.logger.Debugf("Connection:send:<-ctx.Done", "wsURL:%q sid:%v err:%v", c.wsURL, msg.SessionID, ContextErr(ctx))
		return nil
	case <-c.done:
		c.logger.Debugf("Connection:send:<-c.done", "wsURL:%q sid:%v", c.wsURL, msg.SessionID)
		return nil
	}

	// Block waiting for response.
	if recvCh == nil {
		return nil
	}
	tid := c.findTargetIDForLog(msg.SessionID)
	select {
	case msg := <-recvCh:
		var sid target.SessionID
		tid = ""
		if msg != nil {

View on GitHub (pinned to 93accf6570)

Solutions

  1. Detect the shape: error messages containing 'closing communication with browser' plus a websocket.CloseError code — treat as 'target gone', not as a transient fault.
  2. Re-locate pages/targets after any action that can navigate away or close the page (query browser.contexts().pages() again).
  3. Await navigation/close-triggering actions so subsequent calls are not racing them.
  4. In scripts, treat this specific error as non-retryable for the current page: abandon the page and continue the scenario.

Example fix

// before
await page.click('#logout');       // closes the page
await page.screenshot();          // races target destruction

// after
await page.click('#logout');
try {
  await page.screenshot();
} catch (e) {
  if (!String(e).includes('closing communication with browser')) throw e;
  // target legitimately closed; skip screenshot
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await page.click('#self-destruct');
} catch (e) {
  if (/closing communication with browser/.test(String(e))) {
    // target/page was closed by the app — expected; verify via browser.contexts().pages()
    const gone = (await browser.contexts()[0].pages()).length === 0;
    if (!gone) throw e;
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: A CDP send racing a browser-initiated close: the page or target being destroyed (e.g. window.close, navigation replacing the target), browser shutting down, or remote browser restarting, all while a command for that session is in flight.

Common situations: Clicking a link/button whose handler closes the page, then issuing another action on the stale page object; auto-closing popups racing assertions; teardown concurrent with slow operations.

Related errors


AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15). Data as JSON: /api/errors/729009407c95b77d. Report an issue: GitHub.