{"record":{"id":"7892133519ce91bc","repo":"grafana/k6","slug":"iteration-ended-before-page-on-handler-completed-e","errorCode":null,"errorMessage":"iteration ended before page.on handler completed executing","messagePattern":"iteration ended before page\\.on handler completed executing","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/js/modules/k6/browser/browser/page_mapping.go","lineNumber":809,"sourceCode":"\t\tpageEvent, ok := pageEvents[eventName]\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"unknown page on event: %q\", eventName)\n\t\t}\n\n\t\tctx := vu.Context()\n\t\ttq := vu.get(ctx, p.TargetID())\n\n\t\treturn p.On(eventName, func(event common.PageEvent) error {\n\t\t\twait := queueTask(ctx, tq, func() (sobek.Value, error) {\n\t\t\t\t_, err := handle(sobek.Undefined(), vu.Runtime().ToValue(pageEvent.mapp(vu, event)))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"executing page.on('%s') handler: %w\", eventName, err)\n\t\t\t\t}\n\t\t\t\treturn nil, nil\n\t\t\t})\n\t\t\tif pageEvent.wait {\n\t\t\t\tif _, err := wait(); errors.Is(err, context.Canceled) {\n\t\t\t\t\treturn errors.New(\"iteration ended before page.on handler completed executing\")\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n}\n\nfunc parseWaitForFunctionArgs(\n\trt *sobek.Runtime, timeout time.Duration, pageFunc, opts sobek.Value, gargs ...sobek.Value,\n) (string, *common.FrameWaitForFunctionOptions, []any, error) {\n\tpopts, err := parseFrameWaitForFunctionOptions(rt, opts, timeout)\n\tif err != nil {\n\t\treturn \"\", nil, nil, fmt.Errorf(\"parsing waitForFunction options: %w\", err)\n\t}\n\n\tjs := pageFunc.ToString().String()\n\t_, isCallable := sobek.AssertFunction(pageFunc)\n\tif !isCallable {","sourceCodeStart":791,"sourceCodeEnd":827,"githubUrl":"https://github.com/grafana/k6/blob/8d06114777191d21ef885d05c5f8aa9e1ed838ae/internal/js/modules/k6/browser/browser/page_mapping.go#L791-L827","documentation":"k6 wraps every page.on(handler) callback in the iteration's task queue; for events registered with wait=true — which in this mapping is only the 'metric' event (page_mapping.go:784) — k6 blocks until the queued handler finishes. If the VU iteration context is canceled first (default function returned, scenario/duration ended, test interrupted), the wait returns context.Canceled and k6 reports this error (page_mapping.go:808). It means your page.on handler did not complete within the iteration's lifetime.","triggerScenarios":"Registering page.on('metric', handler) where the handler is queued behind other tasks or performs async work, then ending the iteration before it drains: returning from the default export right after browser work, an async handler (or one that offloads data), very short scenarios (--iterations 1), or aborting the run.","commonSituations":"Metric-export handlers that fire-and-forget to an external sink; teardown-time metric events arriving after the last action; CI jobs that cancel k6 mid-run; teams migrating from console.log-style handlers to async exporters.","solutions":["Make the page.on('metric') handler synchronous and fast — collect into an array or push to a sync sink, no awaits","Await every browser call that emits metrics (await page.goto(...), await page.close()) before returning from the default function","Use page.waitForEvent(...) when you need to synchronize on an event instead of doing async work inside page.on","Upgrade k6 — task-queue draining around page.on has been improved across releases; retest on the latest version"],"exampleFix":"// before\npage.on('metric', async (m) => { await pushToInflux(m); }); // still pending at iteration end\n\n// after\nconst buffered = [];\npage.on('metric', (m) => { buffered.push(m); }); // synchronous; flushed in teardown","handlingStrategy":"validation","validationCode":"// structure page.on handlers so they cannot outlive the iteration:\nconst buffered = [];\npage.on('metric', (m) => {\n  buffered.push(m); // synchronous only — no awaits, no fire-and-forget I/O\n});\n// flush `buffered` in teardown, after all browser work completed","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep page.on('metric') handlers synchronous; never await inside them","Await every browser call that can emit metrics before returning from the default function","Use page.waitForEvent when you need to synchronize on an event instead of async work in page.on","Avoid single-iteration scenarios that end immediately after browser activity"],"tags":["browser","page-events","metrics","async","iteration-lifecycle"],"backgroundTag":"iteration-context-canceled","analyzedSha":"8d06114777191d21ef885d05c5f8aa9e1ed838ae","analyzedAt":"2026-08-18T03:05:52.393Z","contentChangedAt":"2026-08-18T03:05:52.393Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}