grafana/k6 · error

iteration ended before page.on handler completed executing

Error message

iteration ended before page.on handler completed executing

What it means

k6 runs page.on(...) handler bodies on the iteration's task queue. Only the 'metric' event is registered with wait: true (page_mapping.go:784), meaning k6 blocks until the queued handler finishes. If the VU iteration's context is canceled first — duration elapsed, scenario ended — wait() returns context.Canceled and k6 converts it to this error so the incomplete handler is reported instead of silently dropped.

Source

Thrown at internal/js/modules/k6/browser/browser/page_mapping.go:808

		pageEvent, ok := pageEvents[eventName]
		if !ok {
			return fmt.Errorf("unknown page on event: %q", eventName)
		}

		ctx := vu.Context()
		tq := vu.get(ctx, p.TargetID())

		return p.On(eventName, func(event common.PageEvent) error {
			wait := queueTask(ctx, tq, func() (sobek.Value, error) {
				_, err := handle(sobek.Undefined(), vu.Runtime().ToValue(pageEvent.mapp(vu, event)))
				if err != nil {
					return nil, fmt.Errorf("executing page.on('%s') handler: %w", eventName, err)
				}
				return nil, nil
			})
			if pageEvent.wait {
				if _, err := wait(); errors.Is(err, context.Canceled) {
					return errors.New("iteration ended before page.on handler completed executing")
				}
			}
			return nil
		})
	}
}

func parseWaitForFunctionArgs(
	ctx context.Context, timeout time.Duration, pageFunc, opts sobek.Value, gargs ...sobek.Value,
) (string, *common.FrameWaitForFunctionOptions, []any, error) {
	popts := common.NewFrameWaitForFunctionOptions(timeout)
	err := popts.Parse(ctx, opts)
	if err != nil {
		return "", nil, nil, fmt.Errorf("parsing waitForFunction options: %w", err)
	}

	js := pageFunc.ToString().String()
	_, isCallable := sobek.AssertFunction(pageFunc)

View on GitHub (pinned to 93accf6570)

Solutions

  1. Keep page.on('metric') handlers short and synchronous — collect data, don't await anything
  2. Give the scenario headroom (slightly longer duration or a small end-of-test wait) so the last metric events flush
  3. Remove listeners before teardown if you no longer need them, so late events are not queued at all
  4. If the race is benign at shutdown, treat this specific message as non-fatal in your results analysis

Example fix

// before
page.on('metric', async (m) => {
  await sendToServer(m); // slow await inside handler
});

// after
const buffer = [];
page.on('metric', (m) => {
  buffer.push(m); // synchronous; flushed later
});
// flush buffer at a controlled point before the iteration ends
Defensive patterns

Strategy: try-catch

Try / catch

page.on('metric', (m) => {
  try {
    collect(m); // keep it synchronous and fast
  } catch (e) {
    // handler errors surface as "executing page.on('metric') handler: ..."
    console.error('metric handler failed:', e.message);
  }
});

Prevention

When it happens

Trigger: A page.on('metric', ...) handler whose queued task is still pending when the iteration ends: the metric event fires during teardown, or the handler does slow work (awaits, processing many entries) while the scenario's duration/iterations run out.

Common situations: Fixed-duration scenarios where a navigation or final request races shutdown; heavy aggregation inside the metric handler; handlers still registered while page.close()/browser.close() runs; tight iteration budgets in constant-arrival-rate scenarios.

Related errors


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