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

  1. Strip non-serializable fields before the call: pass only plain objects, strings, numbers, booleans, arrays, null.
  2. Convert special types explicitly: Date → ISO string, Map/Set → arrays, BigInt → string.
  3. For circular structures, pick the plain fields you need or use a replacer in your own preprocessing.
  4. For functions inside args, send a name/enum string and branch inside the evaluated function instead.
  5. 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

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


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