{"record":{"id":"b3e82301c20a2675","repo":"can1357/oh-my-pi","slug":"page-waitforfunction-timed-out-after-timeoutms","errorCode":null,"errorMessage":"page.waitForFunction() timed out after ${timeoutMs}ms","messagePattern":"page\\.waitForFunction\\(\\) timed out after (.+?)ms","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts","lineNumber":842,"sourceCode":"\t\tconst result = await this.#captureScreenshotPng(this.#runContext?.timeoutMs ?? 30_000);\n\t\treturn opts.encoding === \"base64\" ? result.png_base64 : Buffer.from(result.png_base64, \"base64\");\n\t}\n\n\tasync waitForFunction(\n\t\tfn: string | ((...args: unknown[]) => unknown | Promise<unknown>),\n\t\topts: { timeout?: number; polling?: number } | undefined,\n\t\t...args: unknown[]\n\t): Promise<unknown> {\n\t\tconst timeoutMs = opts?.timeout ?? this.#runContext?.timeoutMs ?? 30_000;\n\t\tconst signal = this.#runContext?.signal;\n\t\tconst pollingMs = typeof opts?.polling === \"number\" ? opts.polling : 200;\n\t\tconst deadline = Date.now() + timeoutMs;\n\t\twhile (Date.now() <= deadline) {\n\t\t\tconst value = typeof fn === \"string\" ? await this.#evalScript<unknown>(fn) : await this.evaluate(fn, ...args);\n\t\t\tif (value) return value;\n\t\t\tawait untilAborted(signal, () => Bun.sleep(pollingMs));\n\t\t}\n\t\tthrow new ToolError(`page.waitForFunction() timed out after ${timeoutMs}ms`);\n\t}\n\n\tasync #evalScript<TResult>(script: string, timeoutMs?: number): Promise<TResult> {\n\t\tconst result = (await this.#request(\"browser.eval\", { script }, timeoutMs)) as CmuxEvalResult;\n\t\treturn result.value as TResult;\n\t}\n\n\tasync #captureScreenshotPng(timeoutMs: number): Promise<CmuxScreenshotResult & { png_base64: string }> {\n\t\tconst result = (await this.#request(\"browser.screenshot\", {}, timeoutMs)) as CmuxScreenshotResult;\n\t\tif (typeof result.png_base64 !== \"string\" || result.png_base64.length === 0) {\n\t\t\tthrow new ToolError(\"cmux browser screenshot response did not include png_base64\");\n\t\t}\n\t\treturn result as CmuxScreenshotResult & { png_base64: string };\n\t}\n\n\tasync #selectorAction<TResult = void>(\n\t\tselector: string,\n\t\taction: string,","sourceCodeStart":824,"sourceCodeEnd":860,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts#L824-L860","documentation":"CmuxTab.waitForFunction(fn, args, opts) evaluates fn in the page every pollingMs until it returns a truthy value, and throws this ToolError when the deadline (timeoutMs or default) expires first. It mirrors Puppeteer's page.waitForFunction semantics: the predicate never became truthy in time.","triggerScenarios":"The polled condition never becomes true — element never appears, async data never loads, or fn throws/returns falsy on every poll; pollingMs/timeout misconfigured (e.g. timeout 0 or too small); fn references page globals that don't exist (evaluated in page context, not Node).","commonSituations":"Waiting for an element the app renders only under conditions that never occurred (feature flag off, backend error); polling a value that requires scrolling to trigger lazy load; predicates referencing Node-side variables instead of page-side ones; fn silently throwing each iteration because a library isn't loaded yet.","solutions":["Increase the timeout option and confirm pollingMs suits the condition (default poll interval may miss slow updates only if the deadline is the issue).","Debug the predicate: run it once via tab.evaluate(fn) and inspect the actual value/error instead of guessing.","Make the predicate defensive — return false instead of throwing when the target doesn't exist yet (e.g. !!document.querySelector(...) style checks).","Ensure the precondition holds (trigger the action, load the data, or scroll) so the condition can ever become truthy."],"exampleFix":"// before: predicate throws while lib not yet loaded\nawait tab.waitForFunction(() => window.chart.getData().length > 0, undefined, { timeout: 5000 });\n// after: defensive predicate, longer timeout\nawait tab.waitForFunction(\n  () => !!window.chart && window.chart.getData().length > 0,\n  undefined,\n  { timeout: 30_000 },\n);","handlingStrategy":"try-catch","validationCode":"// run the predicate once up front to see its real value/error\nconst initial = await tab.evaluate(() => !!document.querySelector(\".result-row\"));\nif (!initial) console.warn(\"predicate currently false; waitForFunction will poll\");","typeGuard":null,"tryCatchPattern":"try {\n  await tab.waitForFunction(() => document.querySelectorAll(\".row\").length > 0, undefined, { timeout: 30_000 });\n} catch (err) {\n  if (err instanceof ToolError && err.message.includes(\"waitForFunction() timed out\")) {\n    const debug = await tab.evaluate(() => ({ rows: document.querySelectorAll(\".row\").length }));\n    throw new Error(`condition never became true: ${JSON.stringify(debug)}`);\n  }\n  throw err;\n}","preventionTips":["Write predicates that return false rather than throw when prerequisites are missing","Reference only page-side globals — predicates run in the browser, not Node","Run the predicate once via tab.evaluate to validate it before polling","Set timeouts from measured page-load behavior, not guesses"],"tags":["browser","timeout","polling","evaluate"],"backgroundTag":"wait-timeout","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}