A colleague wraps existing objects in a JavaScript Proxy expecting a drop-in replacement. Which kinds of objects stop working through the proxy, and why?
answer
- the proxy is not the target
- private fields live on the instance
- internal slots, incompatible receiver
- object-keyed caches miss
- binding methods loses the traps
basics
~20 sA Proxy is a different object from its target, so anything keyed to the target's identity breaks: methods reading #private fields, built-ins with internal slots such as Map, Set, Date, typed arrays and DOM nodes, and lookups in collections keyed by the original object.
solid answer
~50 sA Proxy is transparent for ordinary properties but not for identity. Two families break. First, objects whose methods need state that lives on the instance itself rather than in a property: `#private` fields, and built-ins with internal slots like `Map`, `Set`, `Date`, typed arrays, promises and DOM nodes. Reading `p.get('k')` on a proxied `Map` finds the method fine, then calls it with `this` set to the proxy, which has no internal `Map` data, so it throws a `TypeError` about an incompatible receiver. Second, anything comparing object identity: `proxy !== target`, so a `Map` or `WeakMap` keyed by the target misses, a `Set` will hold both, and `structuredClone`/`postMessage` reject the proxy outright. The usual patch is to bind methods to the target in the `get` trap — which fixes the receiver but means those calls no longer pass through your traps.
go deeper
Remember the one-line rule: a Proxy is a separate object standing in front of the target, so it is never === the target even when every property read forwards correctly.
Explain the receiver failure concretely — a proxied Map's method resolves fine but throws when called, because the data lives in the instance's internal slots and the proxy has none.
Diagnose from a real symptom. Given an 'illegal invocation' or a cache that suddenly misses, trace it to a wrapper, and weigh binding methods to the target against the traps you lose by doing so.
Set the boundary policy: which object kinds your wrapper refuses, where an unwrap hatch is mandatory (serialization, worker transfer, object-keyed caches), and the rule that raw and proxied references must never circulate together.
## Where the transparency ends For an ordinary object whose state lives in ordinary properties, an empty-handler proxy is indistinguishable from the target: reads, writes, deletes and key listing all forward. The illusion breaks in exactly two places — state that is keyed to the object itself rather than stored as a property, and code that compares object identity. ## Private fields ```js class Counter { #n = 0; inc() { return ++this.#n; } } const p = new Proxy(new Counter(), {}); p.inc(); // TypeError: Cannot read private member #n from an object whose class did not declare it ``` Private fields are not properties. They are installed on the instance at construction and can only be found on that exact object. `p.inc` resolves the method through the prototype chain normally, but the call sets `this` to the proxy, and the proxy was never constructed by `Counter`, so it carries no `#n`. No trap can intercept this — private-field access is not a property operation and has no trap. ## Built-ins with internal slots The same shape applies to most built-ins, because their real data lives in internal slots rather than properties: ```js const m = new Proxy(new Map([['k', 1]]), {}); m.get('k'); // TypeError: incompatible receiver const d = new Proxy(new Date(), {}); d.getTime(); // TypeError ``` `Map`, `Set`, `WeakMap`, `Date`, typed arrays, `ArrayBuffer`, promises and DOM nodes all behave this way; a DOM method called on a proxy typically reports an "Illegal invocation". Plain objects, arrays and functions are the well-behaved cases: arrays keep their exotic length behaviour through a proxy, and `typeof proxy` is `"function"` when the target is callable, so a proxied function is still callable and constructible. ## Identity The proxy is a distinct object, and nothing makes it compare equal to the target: - `proxy === target` is `false`, always. - A `Map` or `WeakMap` entry stored under the target is not found when you look it up with the proxy, and vice versa. Caches, dedup sets and registries keyed by object silently miss. - A `Set` given both will hold two entries. - `structuredClone(proxy)` and `postMessage(proxy)` throw a `DataCloneError`; structured serialization only understands recognised object kinds, and a proxy is not one. This is why reactive libraries expose an unwrap helper — Vue calls it `toRaw` — that you must call before sending state to a worker or IndexedDB. Two checks deliberately do see through: `Array.isArray` reports on the target's kind, and `instanceof` works because the default `getPrototypeOf` trap forwards to the target's prototype. `JSON.stringify` also works, because it only performs ordinary property operations, which the traps handle. ## The usual patch, and its price The standard workaround is to bind methods to the real target in the `get` trap: ```js const safe = (obj) => new Proxy(obj, { get(t, key, receiver) { const value = Reflect.get(t, key, receiver); return typeof value === 'function' ? value.bind(t) : value; } }); safe(new Map([['k', 1]])).get('k'); // 1 ``` This works because the method now runs with the real instance as `this`, so private fields and internal slots resolve. But you have traded transparency for it: every call made through those bound methods operates on the target directly, so a `Map`'s writes never touch your traps, `this` inside the object refers to the unwrapped instance so its own self-calls escape, and you have introduced a fresh function object on each read unless you cache the bindings. ## What to do instead Decide up front what your proxy is allowed to wrap. Reactive layers typically refuse anything they cannot handle — class instances, DOM nodes, typed arrays, dates — and either pass them through raw or instrument collection types explicitly by replacing their methods rather than relying on default forwarding. Keep the raw reference reachable through a documented unwrap function for identity-sensitive boundaries: serialization, worker transfer, object-keyed caches, and equality checks. And never let both the raw object and its proxy circulate freely in the same code path, because then equality becomes a coin flip depending on which reference a caller happens to hold.
- Why can no trap fix the private-field failure?Because private-field access is not a property operation. `this.#n` is a lookup in the object's private element list, installed at construction, with no corresponding trap in the Proxy protocol. The proxy simply does not have `#n` and cannot pretend it does. The only route is to run the method with the real instance as `this` — bind it, or do not wrap that object at all.
- Your reactive store must send state into a Web Worker. What do you do about the proxies?Unwrap first. Structured cloning rejects a proxy with a `DataCloneError`, so you resolve every proxy back to its raw target — via the library's unwrap helper or your own target map — and clone that. It is also the right call for performance, since the worker copy needs no reactivity.
- Which built-in checks still see the target through a proxy?`Array.isArray` is specified to look through proxy layers and report on the target's kind. `instanceof` works too, because the default `getPrototypeOf` behaviour forwards, so the prototype chain matches. `typeof` reports `"function"` for a callable target. Identity-based operations — `===`, `Map`/`Set`/`WeakMap` keys, structured cloning — do not.
saying these in an interview costs you the question
- Assumes an empty handler makes a perfect stand-in
- Thinks a get trap can intercept #private access
- Says proxy === target because they share state
- Expects a WeakMap keyed by the target to find the proxy
- Believes structuredClone copies a proxy fine