grafana/k6 · error

waiting for states of element

Error message

waiting for states %v of element %q

What it means

waitForElementStates() (the engine behind el.waitFor({state:...}) and actionability waits) evaluated its injected script but the result was neither a string ('done'/'error:...' protocol) nor a bool. This is an internal contract violation between the injected JS and the Go side, not a wait timeout — timeouts surface as ErrTimedOut.

Solutions

  1. Upgrade k6 to the latest version to clear internal regressions
  2. Reproduce on a static page; if clean there, audit the app for scripts that patch built-ins
  3. Replace the state wait with a polling loop on isVisible()/isEnabled() as a workaround
  4. Report a k6 issue with selector, states, and k6 version

Example fix

// workaround polling
// before
await el.waitFor({ state: 'visible' });

// after
let ok = false;
for (let i = 0; i < 50 && !ok; i++) {
  ok = await el.isVisible();
  if (!ok) await page.waitForTimeout(200);
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await el.waitFor({ state: 'visible' });
} catch (e) {
  if (/waiting for states/.test(String(e))) {
    for (let i = 0; i < 50 && !(await el.isVisible()); i++) {
      await page.waitForTimeout(200);
    }
  } else throw e;
}

Prevention

When it happens

Trigger: Waiting for any element state where the evaluation returns an unexpected type: page-side tampering with the injected script's environment, or a k6 internal regression; all normal states (visible, hidden, enabled, disabled, editable) produce bool or string results.

Common situations: Hostile pages overriding primitives; k6 version skew after upgrades; essentially unreproducible on well-behaved pages.

Related errors


AI-assisted analysis of grafana/k6@3fcf5388d7 (2026-08-18). Data as JSON: /api/errors/63985052a143c6a1. Report an issue: GitHub.

Appendix: source

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

	opts := evalOptions{
		forceCallable: true,
		returnByValue: true,
	}
	result, err := h.evalWithScript(apiCtx, opts, fn, states, timeout.Milliseconds())
	if err != nil {
		return false, errorFromDOMError(err)
	}
	switch v := result.(type) {
	case string: // Either we're done or an error happened (returned as "error:..." from JS)
		if v == resultDone {
			return true, nil
		}
		return false, errorFromDOMError(v)
	case bool:
		return v, nil
	}

	return false, fmt.Errorf(
		"waiting for states %v of element %q", states, reflect.TypeOf(result))
}

// stepIntoFrame steps into an iframe/frame. Due to CORS, we need to perform this
// step outside of the browser (chromium). It returns the frame that it has stepped
// into and the selector to use within that frame.
func (h *ElementHandle) stepIntoFrame(
	apiCtx context.Context, parsedSelector *Selector, frameNavIndex int, opts *FrameWaitForSelectorOptions,
) (*Frame, string, error) {
	// Split selector at frame navigation boundary
	beforeFrame, afterFrame := h.splitSelectorAtFrame(parsedSelector, frameNavIndex)

	// Find the iframe element using the "before frame" selector
	iframeSelector := h.reconstructSelector(beforeFrame)

	iframeHandle, err := h.waitForSelector(apiCtx, iframeSelector, opts)
	if err != nil {
		return nil, "", fmt.Errorf("finding iframe with selector %q: %w", iframeSelector, err)

View on GitHub (pinned to 3fcf5388d7)