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
- Keep page.on('metric') handlers short and synchronous — collect data, don't await anything
- Give the scenario headroom (slightly longer duration or a small end-of-test wait) so the last metric events flush
- Remove listeners before teardown if you no longer need them, so late events are not queued at all
- 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
- Only the 'metric' event blocks on handler completion — keep those handlers synchronous (no sleep, no awaits, no network)
- Design metric handlers to push into a buffer and flush at a controlled point before the iteration ends
- Add duration headroom to browser scenarios so final metric events are not cut off by teardown
- Unregister heavy listeners before page.close()/browser.close()
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
- browser.on promise rejected: %w
- cannot close context as none is active in browser
- ErrFatal
- attaching new page: %w
- unknown browser event: %q, must be %q
AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15).
Data as JSON: /api/errors/7892133519ce91bc.
Report an issue: GitHub.