JavaScript Proxy & Reflect Reference
All 13 Proxy handler traps and every Reflect method — with invariants, edge cases, and a live code runner
| Trap | Triggered by | Category |
|---|---|---|
| get | Reflect.get | Object |
| set | Reflect.set | Object |
| has | Reflect.has | Object |
| deleteProperty | Reflect.deleteProperty | Object |
| apply | Reflect.apply | Function |
| construct | Reflect.construct | Function |
| getPrototypeOf | Reflect.getPrototypeOf | Prototype |
| setPrototypeOf | Reflect.setPrototypeOf | Prototype |
| isExtensible | Reflect.isExtensible | Prototype |
| preventExtensions | Reflect.preventExtensions | Prototype |
| getOwnPropertyDescriptor | Reflect.getOwnPropertyDescriptor | Property Descriptor |
| defineProperty | Reflect.defineProperty | Property Descriptor |
| ownKeys | Reflect.ownKeys | Property Descriptor |
getObjectES2015handler.get(target, prop, receiver)Intercepts property access — `target[prop]`, `target.prop`, or any reflective equivalent. The trap receives the target object, the property key (string or Symbol), and the receiver (the proxy or an object that inherits from the proxy). Return the value to expose for that property.
setObjectES2015handler.set(target, prop, value, receiver)Intercepts property assignment — `target[prop] = value`. Should return `true` to indicate success. Returning `false` (or a falsy value) causes a `TypeError` in strict mode. Use `Reflect.set` to forward to the default behaviour.
hasObjectES2015handler.has(target, prop)Intercepts the `in` operator — `prop in proxy`. Also triggered by `Reflect.has()`, `with` statement blocks, and some array destructuring checks. Return a boolean indicating whether the property should be considered present.
deletePropertyObjectES2015handler.deleteProperty(target, prop)Intercepts the `delete` operator — `delete proxy[prop]`. Also triggered by `Reflect.deleteProperty()`. Return `true` to indicate the property was successfully deleted, `false` to indicate failure (which causes a TypeError in strict mode).
applyFunctionES2015handler.apply(target, thisArg, argumentsList)Intercepts function calls — `proxy(args)`, `proxy.call(ctx, args)`, `proxy.apply(ctx, args)`, and `Reflect.apply()`. The target must be a callable (function). Use this to add logging, argument validation, throttling, or memoisation around function calls.
constructFunctionES2015handler.construct(target, argumentsList, newTarget)Intercepts `new proxy(args)` and `Reflect.construct()`. The target must be a constructor. The `newTarget` argument is the constructor that was originally called with `new` — useful for implementing abstract base classes and factory patterns. Must return an object.
getPrototypeOfPrototypeES2015handler.getPrototypeOf(target)Intercepts `Object.getPrototypeOf(proxy)`, `Object.prototype.__proto__`, `instanceof`, `Reflect.getPrototypeOf()`, and `isPrototypeOf()` checks. Return the prototype to report — an object or null.
setPrototypeOfPrototypeES2015handler.setPrototypeOf(target, proto)Intercepts `Object.setPrototypeOf(proxy, proto)` and `Reflect.setPrototypeOf()`. Return `true` to indicate success, `false` to indicate failure (which causes a TypeError in strict mode). Use `Reflect.setPrototypeOf` to forward to default behaviour.
isExtensiblePrototypeES2015handler.isExtensible(target)Intercepts `Object.isExtensible(proxy)` and `Reflect.isExtensible()`. The return value must match `Object.isExtensible(target)` exactly — this trap is heavily constrained and is mainly useful for logging or asserting invariants rather than modifying behaviour.
preventExtensionsPrototypeES2015handler.preventExtensions(target)Intercepts `Object.preventExtensions(proxy)`, `Object.seal()`, `Object.freeze()`, and `Reflect.preventExtensions()`. Return `true` to indicate success. If you return `true`, then `Object.isExtensible(target)` must also return `false` afterwards — otherwise a TypeError is thrown.
getOwnPropertyDescriptorProperty DescriptorES2015handler.getOwnPropertyDescriptor(target, prop)Intercepts `Object.getOwnPropertyDescriptor(proxy, prop)`, `Reflect.getOwnPropertyDescriptor()`, and operations that check own property descriptors (such as `Object.keys`, property spread). Return a property descriptor object or `undefined` (to indicate the property does not exist).
definePropertyProperty DescriptorES2015handler.defineProperty(target, prop, descriptor)Intercepts `Object.defineProperty(proxy, prop, descriptor)` and `Reflect.defineProperty()`. Also triggered indirectly by object literals with getters/setters, class field declarations, and property attribute changes. Return `true` on success, `false` on failure.
ownKeysProperty DescriptorES2015handler.ownKeys(target)Intercepts `Object.getOwnPropertyNames(proxy)`, `Object.getOwnPropertySymbols(proxy)`, `Object.keys(proxy)`, `Reflect.ownKeys()`, and `for...in` (partially). Return an array of all own property keys (strings and Symbols) that the proxy should appear to have.
You want to validate object properties on assignment — if someone sets user.age = -5, throw an error. You could use a class with setter methods, but then consumers need to call user.setAge(25) instead of user.age = 25. Proxy lets you intercept user.age = 25 directly and validate it, while keeping the natural property syntax. But Proxy has 13 handler traps with specific invariants, and violating an invariant throws a TypeError that’s hard to debug without knowing the rules.
Why This Reference (Not MDN)
MDN documents Proxy traps and Reflect methods on separate pages. This reference covers all 13 traps and their Reflect counterparts side by side — get, set, has, deleteProperty, apply, construct, and 7 more — with invariant rules, edge cases, and live examples on one page.
What Are JavaScript Proxy and Reflect?
Proxy and Reflect are two ES2015 metaprogramming APIs that let you intercept and redefine fundamental operations on JavaScript objects — property access, assignment, function calls, constructor invocations, and more.
Proxywraps an object (the target) with a handler object whose methods (traps) intercept built-in operations on the target.Reflectis a built-in object providing static methods that mirror each Proxy trap — its methods are the “default behaviour” you can call inside traps to forward operations to the target.
Together they enable validation, logging, reactive data binding, virtual properties, access control, and many other patterns — all without modifying the target object itself.
// Basic proxy: log every property access
const obj = { name: "Alice", age: 30 };
const proxy = new Proxy(obj, {
get(target, prop, receiver) {
console.log("reading:", prop);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log("writing:", prop, "=", value);
return Reflect.set(target, prop, value, receiver);
},
});
proxy.name; // reading: name
proxy.age = 31; // writing: age = 31
The 13 Proxy Handler Traps
A handler object can define any combination of these 13 trap methods. Traps you omit default to the target’s built-in behaviour.
| Trap | Triggered by | Category |
|---|---|---|
get | obj.prop, obj[prop], Reflect.get | Object |
set | obj.prop = v, Reflect.set | Object |
has | prop in obj, Reflect.has | Object |
deleteProperty | delete obj.prop, Reflect.deleteProperty | Object |
apply | fn(), fn.call(), Reflect.apply | Function |
construct | new Fn(), Reflect.construct | Function |
getPrototypeOf | Object.getPrototypeOf, instanceof, Reflect.getPrototypeOf | Prototype |
setPrototypeOf | Object.setPrototypeOf, Reflect.setPrototypeOf | Prototype |
isExtensible | Object.isExtensible, Reflect.isExtensible | Prototype |
preventExtensions | Object.preventExtensions/seal/freeze, Reflect.preventExtensions | Prototype |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor, Reflect.getOwnPropertyDescriptor | Property Descriptor |
defineProperty | Object.defineProperty, obj.x = v, Reflect.defineProperty | Property Descriptor |
ownKeys | Object.keys/getOwnPropertyNames/getOwnPropertySymbols, Reflect.ownKeys, for...in | Property Descriptor |
Object Traps
get trap
Intercepts property reads: proxy.prop, proxy[prop], Reflect.get(), and prototype-chain lookups.
// Provide default values for missing properties
const withDefaults = new Proxy({}, {
get(target, prop) {
return prop in target ? target[prop] : `[${String(prop)} not set]`;
}
});
withDefaults.name = "Alice";
console.log(withDefaults.name); // "Alice"
console.log(withDefaults.title); // "[title not set]"
Critical: for non-writable, non-configurable own data properties, the trap must return the same value as the target — violating this throws a TypeError.
set trap
Intercepts property writes: proxy.prop = value, proxy[prop] = value, Reflect.set().
// Validate and coerce incoming values
const schema = new Proxy({}, {
set(target, prop, value) {
if (prop === "age" && (typeof value !== "number" || value < 0)) {
throw new TypeError("age must be a non-negative number");
}
target[prop] = value;
return true; // must return true to signal success
}
});
The trap must return true to indicate success. Returning false causes a TypeError in strict mode.
has trap
Intercepts the in operator: prop in proxy, Reflect.has().
// Numeric range check
const range = new Proxy({}, {
has(target, prop) {
const n = Number(prop);
return Number.isFinite(n) && n >= 1 && n <= 100;
}
});
console.log(50 in range); // true
console.log(150 in range); // false
deleteProperty trap
Intercepts delete: delete proxy.prop, Reflect.deleteProperty().
const guarded = new Proxy({ id: 1, name: "test" }, {
deleteProperty(target, prop) {
if (prop === "id") throw new Error("id is immutable");
return Reflect.deleteProperty(target, prop);
}
});
delete guarded.name; // succeeds
delete guarded.id; // Error: id is immutable
Function Traps
apply trap
Intercepts function calls: fn(), fn.call(), fn.apply(), Reflect.apply(). The target must be callable.
// Throttle a function
function throttle(fn, ms) {
let lastCall = 0;
return new Proxy(fn, {
apply(target, thisArg, args) {
const now = Date.now();
if (now - lastCall >= ms) {
lastCall = now;
return Reflect.apply(target, thisArg, args);
}
console.log("throttled");
}
});
}
construct trap
Intercepts new: new Proxy(), Reflect.construct(). Must return an object.
// Abstract class enforcement
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("Shape is abstract");
}
}
}
// Or via Proxy — prevent direct instantiation
const AbstractShape = new Proxy(Shape, {
construct(target, args, newTarget) {
if (newTarget === AbstractShape) {
throw new Error("Cannot instantiate abstract class");
}
return Reflect.construct(target, args, newTarget);
}
});
Prototype Traps
getPrototypeOf trap
Intercepts Object.getPrototypeOf(), instanceof, isPrototypeOf(), and Reflect.getPrototypeOf(). Must return an object or null.
setPrototypeOf trap
Intercepts Object.setPrototypeOf() and Reflect.setPrototypeOf(). Return true on success.
isExtensible trap
Intercepts Object.isExtensible() and Reflect.isExtensible(). Must return the same boolean as Object.isExtensible(target) — the invariant is strictly enforced.
preventExtensions trap
Intercepts Object.preventExtensions(), Object.seal(), Object.freeze(). If you return true, the target must already be non-extensible.
Property Descriptor Traps
getOwnPropertyDescriptor trap
Intercepts Object.getOwnPropertyDescriptor() and Reflect.getOwnPropertyDescriptor(). Return a descriptor object or undefined.
// Report all properties as non-enumerable (hide from for...in and Object.keys)
const hidden = new Proxy({ a: 1, b: 2 }, {
getOwnPropertyDescriptor(target, prop) {
const desc = Reflect.getOwnPropertyDescriptor(target, prop);
return desc ? { ...desc, enumerable: false } : undefined;
}
});
console.log(Object.keys(hidden)); // [] — none enumerable
console.log(hidden.a); // 1 — still accessible
defineProperty trap
Intercepts Object.defineProperty() and direct property creation (obj.x = 1). Return true on success.
ownKeys trap
Intercepts Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols(), Reflect.ownKeys(), and for...in. Return an array of strings and/or Symbols.
// Filter keys by prefix
const filtered = new Proxy({ x: 1, _y: 2, z: 3 }, {
ownKeys(target) {
return Reflect.ownKeys(target).filter(k => !String(k).startsWith("_"));
}
});
console.log(Object.keys(filtered)); // ["x","z"]
Reflect: The Default Behaviour API
Every Proxy trap has a corresponding Reflect method that performs the default operation. Using Reflect inside traps is the idiomatic and safe way to forward to the target:
// Always use Reflect inside traps to forward correctly
const proxy = new Proxy(target, {
get(t, p, r) { /* intercept, then */ return Reflect.get(t, p, r); },
set(t, p, v, r) { /* intercept, then */ return Reflect.set(t, p, v, r); },
has(t, p) { /* intercept, then */ return Reflect.has(t, p); },
});
Why Use Reflect Instead of Calling the Target Directly?
| Approach | Issue |
|---|---|
target[prop] in get trap | Doesn’t pass receiver → getters see wrong this |
target[prop] = value in set trap | Throws in strict mode instead of returning false |
delete target[prop] in deleteProperty | Throws in strict mode instead of returning false |
Object.getPrototypeOf(target) | Extra overhead and coerces primitives |
Reflect.get/set/deleteProperty/… | Correct semantics, boolean returns, consistent behaviour |
Common Proxy Patterns
Validation Proxy
function validated(target, validators) {
return new Proxy(target, {
set(t, prop, value) {
if (validators[prop]) {
const error = validators[prop](value);
if (error) throw new TypeError(`${prop}: ${error}`);
}
return Reflect.set(t, prop, value);
}
});
}
const user = validated({}, {
age: (v) => (typeof v !== "number" || v < 0) ? "must be a non-negative number" : null,
email: (v) => !v.includes("@") ? "must be a valid email" : null,
});
user.age = 25; // ok
user.email = "alice@example.com"; // ok
Observable / Reactive Data
function observable(target, onChange) {
return new Proxy(target, {
set(t, prop, value) {
const old = t[prop];
const result = Reflect.set(t, prop, value);
if (result && old !== value) onChange(prop, old, value);
return result;
}
});
}
const state = observable({ count: 0 }, (prop, old, next) => {
console.log(`${prop}: ${old} → ${next}`);
});
state.count++; // count: 0 → 1
state.count++; // count: 1 → 2
Read-Only (Immutable) Proxy
function readonly(target) {
const handler = {
set() { throw new TypeError("This object is read-only"); },
deleteProperty() { throw new TypeError("This object is read-only"); },
defineProperty() { throw new TypeError("This object is read-only"); },
};
return new Proxy(target, handler);
}
const config = readonly({ host: "localhost", port: 3000 });
console.log(config.host); // "localhost"
config.host = "remote"; // TypeError: This object is read-only
Logging / Debugging Proxy
function logged(target, label = "proxy") {
return new Proxy(target, {
get(t, p, r) {
console.log(`[${label}] get: ${String(p)}`);
return Reflect.get(t, p, r);
},
set(t, p, v, r) {
console.log(`[${label}] set: ${String(p)} = ${JSON.stringify(v)}`);
return Reflect.set(t, p, v, r);
},
});
}
Invariant Rules Summary
Proxy traps must respect invariants — rules enforced by the JavaScript engine. Violating an invariant always throws a TypeError:
| Trap | Key Invariant |
|---|---|
get | Non-writable + non-configurable property must return its actual value |
set | Non-writable + non-configurable property cannot be assigned a different value |
has | Cannot return false for a non-configurable own property |
deleteProperty | Cannot return true for a non-configurable own property |
getPrototypeOf | If target is non-extensible, must return actual prototype |
isExtensible | Must return the same boolean as Object.isExtensible(target) — no exceptions |
preventExtensions | Cannot return true if target is still extensible |
getOwnPropertyDescriptor | Cannot return undefined for a non-configurable own property |
construct | Must return an object |
ownKeys | Must include all non-configurable own keys; no duplicates |
Frequently Asked Questions
What is the difference between Proxy and Object.defineProperty?
Object.defineProperty defines a single property with custom attributes (enumerable, configurable, writable, get/set). Proxy intercepts all operations on an object at the meta-level — including property access, assignment, the in operator, function calls, and more. Proxy is more powerful but has slightly higher overhead. Vue 3’s reactivity system switched from Object.defineProperty to Proxy for this reason.
Can Proxies be transparent to the target?
Yes — a proxy with an empty handler (new Proxy(target, {})) is fully transparent. Every operation falls through to the target’s built-in behaviour. Traps are only consulted if defined in the handler.
Is there a performance cost to using Proxy?
Yes, there is a measurable overhead compared to direct property access because each trapped operation goes through an extra function call. In hot loops, this can matter. For general application code — validation, logging, reactive systems — the overhead is negligible.
What is a Revocable Proxy?
Proxy.revocable(target, handler) creates a proxy and a revoke function. Calling revoke() makes the proxy throw a TypeError for any subsequent operation — useful for implementing capability-based security where access can be revoked.
const { proxy, revoke } = Proxy.revocable({ x: 1 }, {});
console.log(proxy.x); // 1
revoke();
console.log(proxy.x); // TypeError: Cannot perform 'get' on a proxy that has been revoked
Can Proxy traps intercept internal slots?
No. Proxy cannot intercept operations that go through internal slots (like Map.prototype.size, Date.prototype.getTime, Promise). These operations require the actual object, not a proxy. Wrapping a Map in a Proxy and forwarding in the get trap works, but operations that check internal slots directly will throw.
When should I use Reflect instead of direct target operations?
Always use Reflect inside Proxy traps. Direct operations (target[prop], target[prop] = value) may lose the receiver (wrong this in getters/setters), throw in strict mode instead of returning a boolean, or behave differently than the standard [[Get]]/[[Set]] operations. Reflect methods are the exact specification-compliant implementations of each fundamental operation.