microsoft/playwright · error

Unexpected handle

Error message

Unexpected handle

What it means

During protocol value deserialization, a payload can carry a handle reference (value.h) pointing to a JSHandle transferred between client and server. innerParseSerializedValue throws 'Unexpected handle' when a handle index arrives but no handles array was supplied to resolve it against — i.e. a value referencing an object handle is parsed in a context that does not carry the handles list.

Solutions

  1. If writing protocol code, pass the message's handles array to parseSerializedValue so handle indices resolve.
  2. Remove unsupported values (JSHandles, functions, unserializable objects) from data sent through APIs that do not transfer handles.
  3. Rebuild the repo (npm run build) to fix generated protocol/serializer version skew.
  4. Report upstream if triggered by ordinary public API usage — it indicates an internal invariant violation.

Example fix

// before
const value = parseSerializedValue(payload, undefined);
// after
const value = parseSerializedValue(payload, message.handles);
Defensive patterns

Strategy: try-catch

Validate before calling

// Before parsing: only pass payloads expected to reference handles together with their handles list.
if (payload.h !== undefined && handles === undefined)
  throw new Error('Payload references a handle but no handles array was provided');

Type guard

function referencesHandle(v: any): boolean {
  return v != null && (v.h !== undefined ||
    (v.m?.some?.(referencesHandle)) || (v.se?.some?.(referencesHandle)));
}

Try / catch

let parsed: unknown;
try {
  parsed = parseSerializedValue(payload, handles);
} catch (e) {
  if ((e as Error).message === 'Unexpected handle') {
    throw new Error('Protocol value references a handle in a handle-less context; regenerate protocol artifacts or pass the handles array.');
  }
  throw e;
}

Prevention

When it happens

Trigger: parseSerializedValue (or a nested Map/Set/function entry recursing into innerParseSerializedValue) encounters an 'h' field while its handles parameter is undefined.

Common situations: Custom protocol/channel code or tests hand-constructing serialized payloads, client/server serializer version skew, or passing JSHandles/functions through APIs that do not transfer handles.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of microsoft/playwright@f1d33b5029 (2026-09-21). Data as JSON: /api/errors/2e4dc6d6da8a8b2e. Report an issue: GitHub.

Appendix: source

Thrown at packages/protocol/src/serializers.ts:130

    return result;
  }
  if (value.me !== undefined) {
    const result = new Map();
    refs.set(value.id!, result);
    for (const { k, v } of value.me)
      result.set(innerParseSerializedValue(k, handles, refs, accessChain), innerParseSerializedValue(v, handles, refs, accessChain));
    return result;
  }
  if (value.se !== undefined) {
    const result = new Set();
    refs.set(value.id!, result);
    for (const item of value.se)
      result.add(innerParseSerializedValue(item, handles, refs, accessChain));
    return result;
  }
  if (value.h !== undefined) {
    if (handles === undefined)
      throw new Error('Unexpected handle');
    return handles[value.h];
  }
  if (value.fn !== undefined) {
    const dummy = () => {};
    Object.defineProperty(dummy, 'name', { value: value.fn });
    return dummy;
  }
  throw new Error(`Attempting to deserialize unexpected value${accessChainToDisplayString(accessChain)}: ${value}`);
}

export type HandleOrValue = { h: number } | { fn: string } | { fallThrough: any };
type VisitorInfo = {
  visited: Map<object, number>;
  lastId: number;
  serialize?: ('Map' | 'Set')[];
};

export function serializeValue(value: any, handleSerializer: (value: any) => HandleOrValue, options: { serialize?: ('Map' | 'Set')[] } = {}): SerializedValue {

View on GitHub (pinned to f1d33b5029)