jackwener/OpenCLI · error

page.evaluate(fn) requires a serializable arrow/function exp

Error message

page.evaluate(fn) requires a serializable arrow/function expression

What it means

`serializeFunctionForEval` converts a JS function to source text for `page.evaluate`. It validates the source with regexes for `function` / arrow syntax and rejects native functions (`[native code]`). If `fn.toString()` is not a parseable arrow/function expression — e.g. a bound method, a native builtin, a class method stripped of context, or minified/transpiled output the regex misses — this error is thrown. The library demands serializable source because the function must be stringified and re-parsed in the browser.

Source

Thrown at src/browser/utils.ts:22

type EvaluateFunction = (...args: never[]) => unknown;

function describeJsonError(err: unknown): string {
  return err instanceof Error ? err.message : String(err);
}

/**
 * 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

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Pass an inline arrow function written in the caller: `page.evaluate((x) => x * 2, 5)`.
  2. If you must pass a method, wrap it: `(...args) => obj.method(...args)` so its own source is an arrow.
  3. Never pass bound functions (`fn.bind(...)`) or native builtins directly — wrap them in an arrow.
  4. Move any closure values into evaluate's args array (they must be JSON-serializable) instead of relying on scope capture.
  5. Check for `[native code]` in `fn.toString()` before calling to fail fast with a clearer message.

Example fix

// before
await page.evaluate(handler.bind(ctx)); // [native code] source
// after
await page.evaluate((arg) => ctx.handler(arg), arg);
Defensive patterns

Strategy: validation

Validate before calling

function isSerializableFn(fn: Function): boolean {
  const src = fn.toString().trim();
  return (/^(async\s+)?function[\s(]/.test(src) || /^(async\s*)?(\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/.test(src)) && !src.includes('[native code]');
}
if (!isSerializableFn(fn)) throw new Error('pass an inline arrow function to evaluate');

Type guard

const isPlainFunction = (fn: unknown): fn is (...args: unknown[]) => unknown =>
  typeof fn === 'function' && !fn.toString().includes('[native code]');

Try / catch

try {
  return await page.evaluate(fn, args);
} catch (e) {
  if (String(e.message).includes('serializable arrow/function')) {
    throw new Error('Wrap bound/native functions: page.evaluate((x) => obj.method(x), arg)');
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing `Math.max.bind(null, 1)` or another bound function (toString yields `function () { [native code] }`); passing a native builtin like `parseInt` directly; passing a class method reference whose source doesn't match the arrow/function patterns; passing a function that went through a proxy or was defined via `new Function` in a way that serializes to native code.

Common situations: TypeScript code where `await page.evaluate(fn)` received a method reference instead of an inline arrow; wrappers that forward `arguments`-style callables; bundler output where helper functions became native or exotic; accidentally passing a variable holding `console.log` or similar builtin.

Related errors


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