{"record":{"id":"609c22f3693715da","repo":"grafana/k6","slug":"waiting-for-load-state-q-w","errorCode":null,"errorMessage":"waiting for load state %q: %w","messagePattern":"waiting for load state %q: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/js/modules/k6/browser/common/frame.go","lineNumber":2063,"sourceCode":"\t\ttimeoutCtx,\n\t\tf,\n\t\t[]string{EventFrameAddLifecycle},\n\t\tfunc(data any) bool {\n\t\t\tif le, ok := data.(FrameLifecycleEvent); ok {\n\t\t\t\treturn le.Event == waitUntil\n\t\t\t}\n\t\t\treturn false\n\t\t})\n\tdefer lifecycleEventCancel()\n\n\tif f.hasLifecycleEventFired(waitUntil) {\n\t\treturn nil\n\t}\n\n\tselect {\n\tcase <-lifecycleEvent:\n\tcase <-timeoutCtx.Done():\n\t\treturn fmt.Errorf(\"waiting for load state %q: %w\", state, ContextErr(timeoutCtx))\n\t}\n\n\treturn nil\n}\n\n// WaitForNavigation waits for the given navigation lifecycle event to happen.\n// RegExMatcher should be non-nil to be able to test against a URL pattern in the options.\n//\n//nolint:funlen\nfunc (f *Frame) WaitForNavigation(opts *FrameWaitForNavigationOptions, rm RegExMatcher) (*Response, error) {\n\tf.log.Debugf(\"Frame:WaitForNavigation\",\n\t\t\"fid:%s furl:%s url:%s\", f.ID(), f.URL(), opts.URL)\n\tdefer f.log.Debugf(\"Frame:WaitForNavigation:return\",\n\t\t\"fid:%s furl:%s\", f.ID(), f.URL())\n\n\ttimeoutCtx, timeoutCancel := context.WithTimeout(f.ctx, opts.Timeout)\n\n\t// Create URL matcher based on the pattern","sourceCodeStart":2045,"sourceCodeEnd":2081,"githubUrl":"https://github.com/grafana/k6/blob/93accf6570dcd306ca5e99cc44c393ee3797761b/internal/js/modules/k6/browser/common/frame.go#L2045-L2081","documentation":"Thrown by Frame.WaitForLoadState when the timeout context expires before the requested lifecycle event (EventFrameAddLifecycle matching your state) is observed, unless it already fired (hasLifecycleEventFired short-circuit). The %w wraps ContextErr(timeoutCtx), typically context.DeadlineExceeded. It means the page genuinely never signalled that lifecycle state within the wait timeout.","triggerScenarios":"Waiting for 'networkidle' on a page with persistent connections (WebSockets, SSE, analytics heartbeats) that keep >2 connections open, so networkidle is never emitted; waiting for 'load' on a page with a hanging subresource; a timeout option smaller than the page's real load time.","commonSituations":"SPAs and chat/streaming apps that never reach networkidle; throttled CI networks making load exceed the default 30s; waiting after navigation already completed but for the wrong state.","solutions":["Prefer 'domcontentloaded' or 'load' instead of 'networkidle' for pages with live connections","Increase the timeout: page.waitForLoadState('load', { timeout: 60000 }) or the browser-level default timeout","If 'networkidle' is required, block the offending hosts (browser.setBlockedURLs / context options) so the network can go idle"],"exampleFix":"// before\npage.waitForLoadState('networkidle'); // times out: app holds a websocket open\n\n// after\npage.waitForLoadState('load', { timeout: 60_000 });","handlingStrategy":"retry","validationCode":null,"typeGuard":"function isLoadStateTimeout(e) {\n  return e instanceof Error && /waiting for load state/.test(e.message);\n}","tryCatchPattern":"try {\n  await page.waitForLoadState('load', { timeout: 60_000 });\n} catch (e) {\n  if (isLoadStateTimeout(e)) {\n    await page.waitForLoadState('domcontentloaded'); // degrade gracefully\n  } else throw e;\n}","preventionTips":["Avoid 'networkidle' on streaming/websocket apps","Set explicit generous timeouts for slow environments","Block hosts that keep the network busy if idle detection is required"],"tags":["browser","wait-for-load-state","timeout","lifecycle","networkidle"],"backgroundTag":null,"analyzedSha":"93accf6570dcd306ca5e99cc44c393ee3797761b","analyzedAt":"2026-08-15T21:23:27.118Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}