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

  1. Inspect each argument and ensure it is plain JSON data (string, number, boolean, null, plain arrays/objects).
  2. Replace handles/functions with serializable selectors or primitive identifiers (e.g. pass the selector string and query inside the evaluated function).
  3. Remove circular references (JSON.parse(JSON.stringify(x)) for plain data) and convert BigInt to string/number.
  4. 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.
  5. 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

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


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/bd9d1a78bb718223. Report an issue: GitHub.