grafana/k6 · error

waiting for selector

Error message

waiting for selector %q: %w

What it means

Thrown by the k6 browser module's ElementHandle.waitForSelector(). It wraps failure of the internal frame-scoped wait: no matching element appeared before the timeout, the selector was rejected during compilation, or the frame was navigated away/closed mid-wait. Note: if the wait succeeds but the element is not attached (e.g., state:'attached' semantics), a nil handle can be returned without error — this error is specifically the failure branch.

Solutions

  1. Verify the selector actually matches within this element's subtree — test in devtools with document.querySelector from the element, not from document.
  2. Increase the timeout: el.waitForSelector('.child', { timeout: '60s' }).
  3. Use state: 'attached' when visibility is not required, or state: 'hidden' when waiting for removal (removal waits also use this API).
  4. If the containing frame navigates, re-acquire the frame and element first.

Example fix

// before
const row = await page.$('.order-row');
const badge = await row.waitForSelector('.shipped', { timeout: '2s' }); // data arrives later

// after
const row = await page.waitForSelector('.order-row');
const badge = await row.waitForSelector('.shipped', { state: 'visible', timeout: '30s' });
Defensive patterns

Strategy: validation

Validate before calling

// verify the selector matches within this element's subtree before waiting
const matches = await el.$$eval('*', nodes => nodes.some(n => n.matches('.shipped')));
if (!matches) { throw new Error('selector never matches inside this element'); }
await el.waitForSelector('.shipped', { timeout: '30s' });

Try / catch

try {
  child = await el.waitForSelector('.child', { state: 'visible', timeout: '30s' });
} catch (e) {
  if (String(e).includes('waiting for selector')) {
    await page.screenshot({ path: 'selector-timeout.png' }); // inspect actual DOM
  }
  throw e;
}

Prevention

When it happens

Trigger: await el.waitForSelector('.child', { timeout }) where the child never renders (wrong selector, typo, scoped inside an element that is the wrong ancestor); waiting inside an iframe-embedded element after the iframe navigates; invalid selector syntax failing querySelector compilation; timeout shorter than app render time.

Common situations: Selectors that work in devtools but not scoped to the queried element (waitForSelector searches within the element's subtree); SPAs that render conditionally after async data arrives; slow backends in load tests making default timeouts too tight.

Related errors


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

Appendix: source

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

	return nil
}

// WaitForElementState waits for the element to reach the given state.
func (h *ElementHandle) WaitForElementState(state string, opts *ElementHandleWaitForElementStateOptions) error {
	_, err := h.waitForElementState(h.ctx, []string{state}, opts.Timeout)
	if err != nil {
		return fmt.Errorf("waiting for element state %q: %w", state, err)
	}

	return nil
}

// WaitForSelector waits for the selector to appear in the DOM.
func (h *ElementHandle) WaitForSelector(selector string, opts *FrameWaitForSelectorOptions) (*ElementHandle, error) {
	handle, err := h.waitForSelector(h.ctx, selector, opts)
	if err != nil {
		return nil, fmt.Errorf("waiting for selector %q: %w", selector, err)
	}

	return handle, nil
}

// evalWithScript evaluates the given js code in the scope of this ElementHandle and returns the result.
// The js code can call helper functions from injected_script.js.
func (h *ElementHandle) evalWithScript(
	ctx context.Context,
	opts evalOptions, js string, args ...any,
) (any, error) {
	script, err := h.execCtx.getInjectedScript(h.ctx)
	if err != nil {
		return nil, fmt.Errorf("getting injected script: %w", err)
	}
	return h.eval(ctx, opts, js, append([]any{script}, args...)...)
}

View on GitHub (pinned to 3fcf5388d7)