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.
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.
Understand the failure class
Background: "JSON serialization failed", "not JSON serializable", "Failed to serialize": why JSON marshaling errors happen and how to fix them — this error's family across 46 libraries.
Related errors
- page.evaluate arguments must be JSON-serializable
- page.evaluate(fn) requires a serializable arrow/function…
- GeoGebra object existence probe returned malformed result
- page.evaluate string input does not accept args; use…
- [pipeline/template] sanitizeContext failed
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/bd9d1a78bb718223.
Report an issue: GitHub.
Appendix: 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)