can1357/oh-my-pi · error · ToolError

page.waitForFunction() timed out after ${timeoutMs}ms

Error message

page.waitForFunction() timed out after ${timeoutMs}ms

What it means

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.

Source

Thrown at packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts:842

		const result = await this.#captureScreenshotPng(this.#runContext?.timeoutMs ?? 30_000);
		return opts.encoding === "base64" ? result.png_base64 : Buffer.from(result.png_base64, "base64");
	}

	async waitForFunction(
		fn: string | ((...args: unknown[]) => unknown | Promise<unknown>),
		opts: { timeout?: number; polling?: number } | undefined,
		...args: unknown[]
	): Promise<unknown> {
		const timeoutMs = opts?.timeout ?? this.#runContext?.timeoutMs ?? 30_000;
		const signal = this.#runContext?.signal;
		const pollingMs = typeof opts?.polling === "number" ? opts.polling : 200;
		const deadline = Date.now() + timeoutMs;
		while (Date.now() <= deadline) {
			const value = typeof fn === "string" ? await this.#evalScript<unknown>(fn) : await this.evaluate(fn, ...args);
			if (value) return value;
			await untilAborted(signal, () => Bun.sleep(pollingMs));
		}
		throw new ToolError(`page.waitForFunction() timed out after ${timeoutMs}ms`);
	}

	async #evalScript<TResult>(script: string, timeoutMs?: number): Promise<TResult> {
		const result = (await this.#request("browser.eval", { script }, timeoutMs)) as CmuxEvalResult;
		return result.value as TResult;
	}

	async #captureScreenshotPng(timeoutMs: number): Promise<CmuxScreenshotResult & { png_base64: string }> {
		const result = (await this.#request("browser.screenshot", {}, timeoutMs)) as CmuxScreenshotResult;
		if (typeof result.png_base64 !== "string" || result.png_base64.length === 0) {
			throw new ToolError("cmux browser screenshot response did not include png_base64");
		}
		return result as CmuxScreenshotResult & { png_base64: string };
	}

	async #selectorAction<TResult = void>(
		selector: string,
		action: string,

View on GitHub (pinned to 9690622007)

Solutions

  1. Increase the timeout option and confirm pollingMs suits the condition (default poll interval may miss slow updates only if the deadline is the issue).
  2. Debug the predicate: run it once via tab.evaluate(fn) and inspect the actual value/error instead of guessing.
  3. Make the predicate defensive — return false instead of throwing when the target doesn't exist yet (e.g. !!document.querySelector(...) style checks).
  4. Ensure the precondition holds (trigger the action, load the data, or scroll) so the condition can ever become truthy.

Example fix

// before: predicate throws while lib not yet loaded
await tab.waitForFunction(() => window.chart.getData().length > 0, undefined, { timeout: 5000 });
// after: defensive predicate, longer timeout
await tab.waitForFunction(
  () => !!window.chart && window.chart.getData().length > 0,
  undefined,
  { timeout: 30_000 },
);
Defensive patterns

Strategy: try-catch

Validate before calling

// run the predicate once up front to see its real value/error
const initial = await tab.evaluate(() => !!document.querySelector(".result-row"));
if (!initial) console.warn("predicate currently false; waitForFunction will poll");

Try / catch

try {
  await tab.waitForFunction(() => document.querySelectorAll(".row").length > 0, undefined, { timeout: 30_000 });
} catch (err) {
  if (err instanceof ToolError && err.message.includes("waitForFunction() timed out")) {
    const debug = await tab.evaluate(() => ({ rows: document.querySelectorAll(".row").length }));
    throw new Error(`condition never became true: ${JSON.stringify(debug)}`);
  }
  throw err;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/b3e82301c20a2675. Report an issue: GitHub.