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
- If writing protocol code, pass the message's handles array to parseSerializedValue so handle indices resolve.
- Remove unsupported values (JSHandles, functions, unserializable objects) from data sent through APIs that do not transfer handles.
- Rebuild the repo (npm run build) to fix generated protocol/serializer version skew.
- 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
- Always pass the message's handles array alongside the serialized value.
- Do not send JSHandles/functions through APIs that do not support handle transfer.
- Run npm run build after protocol changes to avoid client/server serializer skew.
- In tests, assert payload/handles pairing before calling parseSerializedValue.
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
- Cannot find command to respond
- Cannot find object to
- Cannot find parent object
- Missing type
- Object with guid was not bound in the connection
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)