microsoft/playwright · error · Error

Cannot serialize result: object reference chain is too long.

Error message

Cannot serialize result: object reference chain is too long.

What it means

Rewritten in rewriteError() (crExecutionContext.ts:104) when the underlying CDP error message contains 'Object reference chain is too long' or 'CBOR: stack limit exceeded'. The protocol serializer hit its reference-depth limit while materializing the result of an evaluate.

Source

Thrown at packages/playwright-core/src/server/chromium/crExecutionContext.ts:104

      result.set(property.name, createHandle(object._context, property.value));
    }
    return result;
  }

  async releaseHandle(handle: js.JSHandle): Promise<void> {
    if (!handle._objectId)
      return;
    await releaseObject(this._client, handle._objectId);
  }

  shouldPrependErrorPrefix(): boolean {
    return false;
  }
}

function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
  if (error.message.includes('Object reference chain is too long') || error.message.includes('CBOR: stack limit exceeded'))
    throw new Error('Cannot serialize result: object reference chain is too long.');
  if (error.message.includes('Object couldn\'t be returned by value'))
    return { result: { type: 'undefined' } };
  if (error.message.includes('Promise was collected'))
    throw new Error('Resulting promise was garbage collected.');

  if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
    rewriteErrorMessage(error, error.message + ' Are you passing a nested JSHandle?');
  if (!js.isJavaScriptErrorInEvaluate(error) && !isSessionClosedError(error))
    throw new Error('Execution context was destroyed, most likely because of a navigation.');
  throw error;
}

function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObject): any {
  const value = remoteObject.value;
  const unserializableValue = remoteObject.unserializableValue;
  return unserializableValue ? js.parseUnserializableValue(unserializableValue) : value;
}

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Project the data to a small/plain shape inside the page before returning.
  2. Return a handle via page.evaluateHandle(() => big) instead of by-value evaluate.
  3. If you need fields, read them individually with handle.getProperty and jsonValue on primitives.

Example fix

// before
const state = await page.evaluate(() => window.__bigStore__);
// after
const handle = await page.evaluateHandle(() => window.__bigStore__);
const summary = await handle.evaluate(s => ({ users: s.users.length, ready: s.ready }));
Defensive patterns

Strategy: validation

Validate before calling

// Project to a small shape inside the page; fall back to a handle for large graphs.
const small = await page.evaluate(() => {
  const s = (window as any).__bigStore__;
  return { count: s?.items?.length ?? 0, ready: !!s?.ready };
});
// Only if you really need the whole object:
const handle = await page.evaluateHandle(() => (window as any).__bigStore__);

Prevention

When it happens

Trigger: page.evaluate(() => hugeOrDeeplyNestedObject); returning window, document, a framework's component tree, or a cyclic graph by value; jsHandle.getProperty chains projected back as values.

Common situations: Returning the whole Vue/React root from evaluate; returning large state stores; debugging helpers that dump nested configs.

Related errors


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/1f3c82f6fee65060. Report an issue: GitHub.