jackwener/OpenCLI · error
page.evaluate arguments must be JSON-serializable: ${describ
Error message
page.evaluate arguments must be JSON-serializable: ${describeJsonError(err)} What it means
After validating the function source, `serializeFunctionForEval` JSON-stringifies the evaluate arguments. If an argument contains values JSON cannot represent — functions, Symbols, undefined at object positions, circular references, BigInt, class instances with such fields — `JSON.stringify` throws and this error wraps the underlying cause via `describeJsonError`. Browser evaluate boundaries only carry structured-clone/JSON data, so arguments must be plain serializable values.
Source
Thrown at src/browser/utils.ts:29
/**
* 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-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';View on GitHub (pinned to 49907e53dc)
Solutions
- Strip non-serializable fields before the call: pass only plain objects, strings, numbers, booleans, arrays, null.
- Convert special types explicitly: Date → ISO string, Map/Set → arrays, BigInt → string.
- For circular structures, pick the plain fields you need or use a replacer in your own preprocessing.
- For functions inside args, send a name/enum string and branch inside the evaluated function instead.
- Inspect the wrapped `describeJsonError` message to locate the offending argument and path.
Example fix
// before
await page.evaluate(fn, { el: domNode, done: () => {} });
// after
await page.evaluate(fn, { selector: '#target', doneEvent: 'ready' }); Defensive patterns
Strategy: validation
Validate before calling
function assertJsonSafe(value: unknown, path = 'args'): void {
if (value === undefined) throw new Error(`${path} is undefined`);
if (typeof value === 'function' || typeof value === 'symbol' || typeof value === 'bigint') throw new Error(`${path} is ${typeof value}`);
if (value === null || typeof value !== 'object') return;
if (seen.has(value)) throw new Error(`${path} is circular`);
seen.add(value);
for (const [k, v] of Object.entries(value)) assertJsonSafe(v, `${path}.${k}`);
seen.delete(value);
}
const seen = new Set();
assertJsonSafe(args); Type guard
const isJsonSafe = (v: unknown): v is string | number | boolean | null | JsonSafe[] | { [k: string]: JsonSafe } => {
try { JSON.stringify(v); return true; } catch { return false; }
}; Try / catch
try {
return await page.evaluate(fn, args);
} catch (e) {
if (String(e.message).includes('JSON-serializable')) {
console.error('Non-serializable evaluate args:', e.message);
throw new Error('Strip functions/DOM nodes/circular refs from evaluate args');
}
throw e;
} Prevention
- Validate args with JSON.stringify(args) in a try/catch before calling evaluate.
- Convert Date→ISO string, Map/Set→array, BigInt→string at the boundary.
- Never pass callbacks, DOM nodes, or class instances with methods inside args.
- Send function selectors as strings and branch inside the page function.
When it happens
Trigger: Passing a callback or function inside args (`page.evaluate(fn, { onDone: () => {} })`); passing DOM nodes, Date is OK but Map/Set/BigInt/circular structures are not; passing class instances containing Symbol keys or circular references; accidentally passing `undefined` as a lone argument (hits the `serializedArgs === undefined` branch).
Common situations: React/testing code passing element handles or state objects with embedded functions; configs carrying logger functions; passing Error objects (non-enumerable message/stack) into the page; migrating from APIs that accepted a serialize parameter (like puppeteer's deprecated option) that this library does not support.
Related errors
- page.evaluate arguments must be JSON-serializable
- page.evaluate(fn) requires a serializable arrow/function exp
- [pipeline/template] sanitizeContext failed: ${err instanceof
- 12306 ${endpoint} returned an unexpected payload shape
- ${label} returned a non-JSON response
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/1a72358f76f1b33e.
Report an issue: GitHub.