grafana/k6 · warning

disposing browser context ID

Error message

disposing browser context ID %s: %w

What it means

BrowserContext.Close() sends Target.disposeBrowserContext over CDP to tear down the context in the browser. This error wraps the browser rejecting or failing that command. It is most often a teardown-ordering artifact: the context is already gone (double close, browser already closing/closed) or the CDP session itself is dead.

Solutions

  1. Close each context exactly once, before closing the browser, and await the close
  2. Track a closed flag per context and skip redundant close calls
  3. Treat close errors during teardown as warnings: log and continue instead of failing the run
  4. Check K6_BROWSER_LOG=debug for a preceding EventConnectionClose indicating the browser died first

Example fix

// before
await context.close();
await context.close(); // double close -> disposing browser context ID ...

// after
let closed = false;
async function closeContext() {
  if (closed) return;
  closed = true;
  await context.close();
}
Defensive patterns

Strategy: try-catch

Try / catch

// During teardown, tolerate context disposal errors
try {
  await context.close();
} catch (e) {
  if (/disposing browser context ID/.test(String(e))) {
    console.warn('context already disposed or browser gone; ignoring');
  } else throw e;
}

Prevention

When it happens

Trigger: Calling context.close() twice; calling context.close() after browser.close() started; closing a context while the WS connection is already lost (browser crashed); disposing a context id the browser no longer knows.

Common situations: finally blocks that close contexts while k6 teardown also closes the browser; double-close from test helpers; closing contexts during an aborted test run.

Related errors


AI-assisted analysis of grafana/k6@3fcf5388d7 (2026-09-20). Data as JSON: /api/errors/4fed44a28a3884b7. Report an issue: GitHub.

Appendix: source

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

	if err != nil {
		return fmt.Errorf("browser connect: %w", err)
	}
	// The connection's recvLoop reads defaultContext through
	// connectionOnAttachedToTarget, so publish it under contextMu.
	b.contextMu.Lock()
	b.defaultContext = defaultContext
	b.contextMu.Unlock()
	b.runOnClose = append(b.runOnClose, defaultContext.cleanup)

	return b.initEvents()
}

func (b *Browser) disposeContext(id cdp.BrowserContextID) error {
	b.logger.Debugf("Browser:disposeContext", "bctxid:%v", id)

	action := target.DisposeBrowserContext(id)
	if err := action.Do(cdp.WithExecutor(b.vuCtx, b.conn)); err != nil {
		return fmt.Errorf("disposing browser context ID %s: %w", id, err)
	}
	b.contextMu.Lock()
	b.context = nil
	b.contextMu.Unlock()

	return nil
}

// getDefaultBrowserContextOrMatchedID returns the BrowserContext for the given browser context ID.
// If the browser context is not found, the default BrowserContext is returned.
func (b *Browser) getDefaultBrowserContextOrMatchedID(id cdp.BrowserContextID) *BrowserContext {
	b.contextMu.RLock()
	defer b.contextMu.RUnlock()

	if b.context == nil || b.context.id != id {
		return b.defaultContext
	}

View on GitHub (pinned to 3fcf5388d7)