JavaScript Proxy Object
debt(d8/e6/b6/t8)
Closest to 'silent in production until users hit it' (d8). detection_hints.automated is no, and the regex only catches the literal new Proxy pattern. Internal slot bypass, missing Reflect delegation, and identity bugs typically only surface at runtime when specific code paths hit them, with no standard linter rule catching these.
Closest to 'cross-cutting refactor across the codebase' (e6). quick_fix says to delegate every trap to Reflect and avoid wrapping internal-slot objects — but if a Proxy is already woven into reactivity/observability layers, fixing it often requires touching every trap and every call site that compares identity or stores keys, which is more than a single component.
Closest to 'strong gravitational pull' (b6). applies_to spans web/node/library and tags include reactivity — a Proxy at the heart of a reactivity or validation system shapes how all consumers interact with data (identity, equality, method binding), but it's not necessarily the whole system's shape, hence slightly below b7.
Closest to 'serious trap' (t8). The misconception is explicit: developers reasonably assume traps fire for every operation, but internal slot access bypasses them entirely, causing TypeErrors on Map/Set/Date/#private. This directly contradicts the intuitive mental model of 'proxy intercepts everything', pushing it toward catastrophic.
Also Known As
TL;DR
Explanation
The Proxy constructor (ES2015) creates a transparent wrapper around a target object. You define a handler object whose methods (called traps) intercept operations like get, set, has, deleteProperty, apply, construct, ownKeys, and getOwnPropertyDescriptor. Reading or writing through the proxy invokes the matching trap; omitted traps fall through to default behaviour on the target.
Common use cases include validation (reject writes that violate a schema), reactive systems (notify on property changes - the foundation of Vue 3 reactivity), negative array indices, default values for missing properties, logging and debugging, virtual properties that compute on access, access control, and immutable views. Reflect provides a companion API whose methods mirror trap signatures, so handlers usually delegate to Reflect.get(target, prop, receiver) etc. to preserve correct semantics around inheritance and receiver binding.
Proxies have caveats. They are not transparent for identity-sensitive code: proxy !== target. Some built-in operations bypass traps when they access internal slots directly - wrapping a Map or Date and then calling its methods on the proxy will usually throw because the methods need the original internal slot. Private class fields also bypass proxies for the same reason. Performance is meaningfully worse than direct property access; do not wrap hot data structures without measuring. Proxies cannot be revoked unless created via Proxy.revocable, and once revoked any access throws TypeError.
Unlike Object.defineProperty (which intercepts known properties one at a time), Proxy intercepts all properties including ones added after creation, which is why modern reactive frameworks moved to it. Compared to PHP magic methods like __get and __set, Proxy traps are more uniform and complete - they cover deletion, enumeration, and prototype operations as well.
Common Misconception
Why It Matters
Common Mistakes
- Forgetting to delegate to Reflect inside traps, breaking receiver semantics and inherited getters/setters.
- Wrapping Map, Set, Date, or other objects with internal slots and then calling their methods through the proxy (TypeError on internal slot access).
- Comparing proxy to target with === and being surprised they differ; storing the target as a map key while looking up the proxy.
- Adding heavy logic in get/set traps on hot paths, causing severe performance regressions.
- Assuming private class fields (#field) are observable through a proxy - they bypass traps entirely.
Code Examples
// Naive trap - breaks inherited getters and `this` binding
const user = new Proxy(target, {
get(obj, prop) {
return obj[prop]; // ignores receiver, no Reflect
},
set(obj, prop, value) {
obj[prop] = value;
return true;
}
});
// Wrapping a Map - looks fine, throws at runtime
const m = new Proxy(new Map(), {});
m.set('a', 1); // TypeError: Method Map.prototype.set called on incompatible receiver
// Delegate through Reflect, validate on set
const schema = { name: 'string', age: 'number' };
function validated(target) {
return new Proxy(target, {
get(obj, prop, receiver) {
return Reflect.get(obj, prop, receiver);
},
set(obj, prop, value, receiver) {
if (prop in schema && typeof value !== schema[prop]) {
throw new TypeError(`${prop} must be ${schema[prop]}`);
}
return Reflect.set(obj, prop, value, receiver);
}
});
}
// Revocable proxy for time-limited access
const { proxy, revoke } = Proxy.revocable({ secret: 42 }, {});
console.log(proxy.secret); // 42
revoke();
// proxy.secret -> TypeError
// For Map/Set, rebind methods so internal slot access works
const raw = new Map();
const wrapped = new Proxy(raw, {
get(obj, prop, receiver) {
const v = Reflect.get(obj, prop, receiver);
return typeof v === 'function' ? v.bind(obj) : v;
}
});