jackwener/OpenCLI · error
page.evaluate arguments must be JSON-serializable
Error message
page.evaluate arguments must be JSON-serializable
What it means
serializeFunctionForEval JSON.stringify's the caller-supplied args array before embedding them into a page.evaluate expression. JSON.stringify throws on values the structured-clone-free JSON format cannot represent (functions, circular structures, BigInt, symbols in objects). The library throws this error so the failure is reported before any browser evaluation happens, naming the offending argument.
Source
Thrown at src/browser/utils.ts:32
* 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-is
* - Arrow/function literal → wrap as IIFE `(code)()`
* - `new Promise(...)` or raw expression → send as-is (expression)
*/
export function wrapForEval(js: string): string {
if (typeof js !== 'string') return 'undefined';
const code = js.trim();
if (!code) return 'undefined';
// Already an IIFE: `(async () => { ... })()` or `(function() {...})()`
if (/^\([\s\S]*\)\s*\(.*\)\s*$/.test(code)) return code;View on GitHub (pinned to 49907e53dc)
Solutions
- Inspect each argument and ensure it is plain JSON data (string, number, boolean, null, plain arrays/objects).
- Replace handles/functions with serializable selectors or primitive identifiers (e.g. pass the selector string and query inside the evaluated function).
- Remove circular references (JSON.parse(JSON.stringify(x)) for plain data) and convert BigInt to string/number.
- If you truly need DOM access, use locator APIs or evaluate with element handles via the driver's native evaluate, not this eval-string serializer.
- Read describeJsonError in the thrown message to find the exact argument position that failed.
Example fix
// before await page.evaluate((el) => el.textContent, someElementHandle); // after const text = await page.evaluate((sel) => document.querySelector(sel)?.textContent, '.item');
Defensive patterns
Strategy: validation
Validate before calling
function isJsonSerializable(v, seen = new Set()) {
if (v === undefined || typeof v === 'function' || typeof v === 'symbol' || typeof v === 'bigint') return false;
if (typeof v !== 'object' || v === null) return true;
if (seen.has(v)) return false; // circular
seen.add(v);
return Object.values(v).every(x => isJsonSerializable(x, seen));
}
if (!args.every(a => isJsonSerializable(a))) throw new Error('args must be JSON-serializable'); Type guard
const isJsonSerializable = (v: unknown, seen = new Set<unknown>()): v is JsonValue => v === null || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || (Array.isArray(v) && v.every(x => isJsonSerializable(x, seen))) || (typeof v === 'object' && v !== null && !seen.has(v) && Object.values(v).every(x => isJsonSerializable(x, new Set(seen).add(v))));
Try / catch
try {
const expr = buildEvaluateExpression(fn, args);
} catch (err) {
if (err instanceof Error && err.message.includes('JSON-serializable')) {
console.error('Non-serializable evaluate argument:', args);
}
throw err;
} Prevention
- Only pass plain JSON data (strings, numbers, booleans, null, plain objects/arrays) into evaluate.
- Pass selector strings, not ElementHandles, when using this eval-string serializer.
- Strip circular refs with JSON.parse(JSON.stringify(data)) for plain payloads.
- Never pass functions, DOM nodes, BigInts, or Symbols as evaluate arguments.
When it happens
Trigger: Calling buildEvaluateExpression(fn, args) or page.evaluate(fn, ...args) where any arg is a DOM node, a function, a class instance with circular references, a BigInt, or contains a symbol-keyed property that breaks JSON.stringify; also calling with a single non-serializable arg.
Common situations: Passing a Playwright/Puppeteer ElementHandle or Locator as an argument; passing a function like setTimeout or a callback; passing a Date-heavy object graph with cycles; passing values returned from other non-serializable APIs.
Related errors
- page.evaluate(fn) requires a serializable arrow/function exp
- page.evaluate arguments must be JSON-serializable: ${describ
- GeoGebra object existence probe returned malformed result
- Reddit whoami failed: ${result.detail}
- page.evaluate string input does not accept args; use page.ev
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/bd9d1a78bb718223.
Report an issue: GitHub.