grafana/k6 · error

waiting for function, getting handle: %w

Error message

waiting for function, getting handle: %w

What it means

waitForFunction's next step evaluates the user-supplied predicate JS itself (non-callable, returnByValue=false) to obtain a handle; failure is wrapped as 'waiting for function, getting handle: <cause>'. Causes include syntax errors in the predicate string, the context dying before evaluation, or serialization failure of the function.

Source

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

	if execCtx == nil {
		return nil, fmt.Errorf("waiting for function: execution context %q not found", world)
	}
	injected, err := execCtx.getInjectedScript(apiCtx)
	if err != nil {
		return nil, fmt.Errorf("getting injected script: %w", err)
	}

	pageFn := `
		(injected, predicate, polling, timeout, ...args) => {
			return injected.waitForPredicateFunction(predicate, polling, timeout, ...args);
		}
	`

	// First evaluate the predicate function itself to get its handle.
	opts := evalOptions{forceCallable: false, returnByValue: false}
	handle, err := execCtx.eval(apiCtx, opts, js)
	if err != nil {
		return nil, fmt.Errorf("waiting for function, getting handle: %w", err)
	}

	// Then evaluate the injected function call, passing it the predicate
	// function handle and the rest of the arguments.
	opts = evalOptions{forceCallable: true, returnByValue: false}
	result, err := execCtx.eval(
		apiCtx, opts, pageFn, append([]any{
			injected,
			handle,
			polling,
			timeout.Milliseconds(), // The JS value is in ms integers
		}, args...)...)
	if err != nil {
		return nil, fmt.Errorf("waiting for function, polling: %w", err)
	}
	// prevent passing a non-nil interface to the upper layers.
	if result == nil {
		return nil, nil //nolint:nilnil

View on GitHub (pinned to 93accf6570)

Solutions

  1. Pass a plain function reference: waitForFunction(() => document.querySelector('.done') !== null) — no closure over external variables, no string surgery.
  2. If using a string, make it a full function expression: waitForFunction('() => window.ready === true').
  3. Move dynamic values into args: waitForFunction((v) => window.value === v, expectedValue).
  4. Wait for domcontentloaded before polling to avoid the navigation race.

Example fix

// before
await frame.waitForFunction(`window.count == ${n}`); // not a function, closure leak

// after
await frame.waitForFunction((v) => window.count === v, n, { timeout: 30_000 });
Defensive patterns

Strategy: validation

Validate before calling

// predicate must be a self-contained function; pass data via args
const pred = () => window.count === undefined || window.count >= 3;
await frame.waitForLoadState('domcontentloaded');

Type guard

const isFn = (f) => typeof f === 'function';
if (!isFn(pred)) throw new TypeError('predicate must be a function');

Try / catch

try { await frame.waitForFunction(pred, arg, { timeout }); }
catch (e) { if (/getting handle/.test(e.message)) { /* fix predicate shape: function expr, no closures, args for data */ } throw e; }

Prevention

When it happens

Trigger: Passing a malformed predicate (e.g. waitForFunction('window.x' as a plain expression string that is not a function, unbalanced braces, or PageFunction shape the module cannot serialize); navigation destroying the context between the previous step and this eval.

Common situations: Building predicate strings dynamically with bad interpolation; passing an arrow function that closes over k6-side variables (not serializable); racing page transitions at poll start.

Related errors


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