grafana/k6 · error

sending a message to browser: %w

Error message

sending a message to browser: %w

What it means

Connection.send publishes a CDP message and simultaneously watches errorCh; if the sendLoop reports a websocket write error (broken/aborted connection), send fails with this wrapper. It means the command never (reliably) reached the browser — the transport was already broken when k6 tried to write.

Source

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

	msg := &cdproto.Message{
		ID:        c.msgIDGen.newID(),
		SessionID: sid,
		Method:    cdproto.MethodType(cdpruntime.CommandRunIfWaitingForDebugger),
	}
	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 {

View on GitHub (pinned to 93accf6570)

Solutions

  1. Check whether the browser process is still alive and consult its logs for the crash reason.
  2. For remote browsers, keepalive/tune LB idle timeouts so ws connections are not reaped mid-test.
  3. Make scripts resilient: catch the error, re-acquire page/context, and retry the logical action once.
  4. If it recurs at a specific step, capture K6_BROWSER_ENABLE_DEBUGGING output to see which CDP domain call breaks.

Example fix

// before
await page.goto('https://example.com');  // throws 'sending a message to browser: ...' if ws broke

// after
async function gotoRetry(page, url, tries = 2) {
  for (let i = 0; i < tries; i++) {
    try { return await page.goto(url); }
    catch (e) {
      if (i === tries - 1) throw e;
      await sleep(1000);
    }
  }
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await page.goto(url, { waitUntil: 'load' });
} catch (e) {
  if (/sending a message to browser/.test(String(e))) {
    // transport was broken: re-open a page (or context) and retry the logical step once
    const p2 = await browser.newPage();
    await p2.goto(url, { waitUntil: 'load' });
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Any CDP operation (page.goto, click, evaluate, screenshot…) issued after or exactly while the websocket breaks: browser crashed mid-test, connection reset by the remote browser, or network interruption to a remote browser.

Common situations: Browser OOM-killed mid-scenario; remote browser pod evicted; long-running tests where an idle connection is reaped by a load balancer; operations attempted after a prior unnoticed close.

Related errors


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