jackwener/OpenCLI · error
page.evaluate(fn) requires a serializable arrow/function exp
Error message
page.evaluate(fn) requires a serializable arrow/function expression
What it means
`serializeFunctionForEval` converts a JS function to source text for `page.evaluate`. It validates the source with regexes for `function` / arrow syntax and rejects native functions (`[native code]`). If `fn.toString()` is not a parseable arrow/function expression — e.g. a bound method, a native builtin, a class method stripped of context, or minified/transpiled output the regex misses — this error is thrown. The library demands serializable source because the function must be stringified and re-parsed in the browser.
Source
Thrown at src/browser/utils.ts:22
type EvaluateFunction = (...args: never[]) => unknown;
function describeJsonError(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
/**
* Serialize a function-form page.evaluate call for CDP Runtime.evaluate.
*
* Functions execute in the browser page context, so they cannot close over
* Node-side variables. Pass external values as JSON-serializable args instead.
*/
export function serializeFunctionForEval(fn: EvaluateFunction, args: readonly unknown[] = []): string {
const source = fn.toString().trim();
const isFunctionSource = /^(async\s+)?function[\s(]/.test(source)
|| /^(async\s*)?(\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/.test(source);
if (!isFunctionSource || source.includes('[native code]')) {
throw new Error('page.evaluate(fn) requires a serializable arrow/function expression');
}
let serializedArgs: string;
try {
serializedArgs = JSON.stringify(args);
} catch (err) {
throw new Error(`page.evaluate arguments must be JSON-serializable: ${describeJsonError(err)}`);
}
if (serializedArgs === undefined) {
throw new Error('page.evaluate arguments must be JSON-serializable');
}
return `(${source})(...${serializedArgs})`;
}
/**
* Wrap JS code for CDP Runtime.evaluate:
* - Already an IIFE `(...)()` → send as-isView on GitHub (pinned to 49907e53dc)
Solutions
- Pass an inline arrow function written in the caller: `page.evaluate((x) => x * 2, 5)`.
- If you must pass a method, wrap it: `(...args) => obj.method(...args)` so its own source is an arrow.
- Never pass bound functions (`fn.bind(...)`) or native builtins directly — wrap them in an arrow.
- Move any closure values into evaluate's args array (they must be JSON-serializable) instead of relying on scope capture.
- Check for `[native code]` in `fn.toString()` before calling to fail fast with a clearer message.
Example fix
// before await page.evaluate(handler.bind(ctx)); // [native code] source // after await page.evaluate((arg) => ctx.handler(arg), arg);
Defensive patterns
Strategy: validation
Validate before calling
function isSerializableFn(fn: Function): boolean {
const src = fn.toString().trim();
return (/^(async\s+)?function[\s(]/.test(src) || /^(async\s*)?(\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/.test(src)) && !src.includes('[native code]');
}
if (!isSerializableFn(fn)) throw new Error('pass an inline arrow function to evaluate'); Type guard
const isPlainFunction = (fn: unknown): fn is (...args: unknown[]) => unknown =>
typeof fn === 'function' && !fn.toString().includes('[native code]'); Try / catch
try {
return await page.evaluate(fn, args);
} catch (e) {
if (String(e.message).includes('serializable arrow/function')) {
throw new Error('Wrap bound/native functions: page.evaluate((x) => obj.method(x), arg)');
}
throw e;
} Prevention
- Always author evaluate callbacks as inline arrows in the calling code.
- Never pass .bind() results or native builtins directly to evaluate.
- Move closure-captured values into the args array.
- Lint for evaluate(fn) calls where fn is a method reference.
When it happens
Trigger: Passing `Math.max.bind(null, 1)` or another bound function (toString yields `function () { [native code] }`); passing a native builtin like `parseInt` directly; passing a class method reference whose source doesn't match the arrow/function patterns; passing a function that went through a proxy or was defined via `new Function` in a way that serializes to native code.
Common situations: TypeScript code where `await page.evaluate(fn)` received a method reference instead of an inline arrow; wrappers that forward `arguments`-style callables; bundler output where helper functions became native or exotic; accidentally passing a variable holding `console.log` or similar builtin.
Related errors
- page.evaluate arguments must be JSON-serializable
- GeoGebra object existence probe returned malformed result
- page.evaluate arguments must be JSON-serializable: ${describ
- page.evaluate string input does not accept args; use page.ev
- ${label} failed: ${error?.message ?? error}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/0b770a80e29d55b1.
Report an issue: GitHub.