Technology · JavaScript
AdvancedJavaScript Proxy and Reflect
Learn how Proxy traps and the Reflect API intercept and control object behavior in JavaScript.
TL;DR
- 01Wrap objects with Proxy to intercept reads, writes, and calls.
- 02Define traps like get and set to customize behavior.
- 03Use Reflect to forward default behavior inside trap handlers.
Creating a Proxy
- Wrap any object with
new Proxy(target, handler)to intercept operations performed on it.const target = { name: "Ada" }; const proxy = new Proxy(target, {}); proxy.name; // "Ada" — no traps defined, so default behavior applies - The
handlerobject holds trap functions; any operation without a matching trap falls through to the target.const handler = { get(target, prop) { console.log(`reading ${prop}`); return target[prop]; } }; const proxy = new Proxy({ x: 1 }, handler); proxy.x; // logs "reading x", returns 1 - A Proxy behaves like the original object externally, so existing code can use it without changes.
const user = new Proxy({ name: "Ada" }, {}); typeof user; // "object" JSON.stringify(user); // '{"name":"Ada"}' - Proxies can wrap functions too, since functions are objects in JavaScript.
function greet(name) { return `Hi, ${name}`; } const proxied = new Proxy(greet, { apply(target, thisArg, args) { console.log("called with", args); return target(...args); } }); proxied("Ada"); // logs "called with [\"Ada\"]", returns "Hi, Ada" - Once created, a Proxy's traps stay active for the lifetime of every interaction with that wrapper.
const logged = new Proxy({}, { set(target, prop, value) { console.log(`${prop} = ${value}`); return Reflect.set(target, prop, value); } }); logged.a = 1; // logs "a = 1"
Common Traps
- The
gettrap intercepts property reads, including method calls and inherited properties.const proxy = new Proxy({ a: 1 }, { get(target, prop) { return prop in target ? target[prop] : `no ${String(prop)}`; } }); proxy.a; // 1 proxy.b; // "no b" - The
settrap intercepts property writes and must returntrueto signal success.const proxy = new Proxy({}, { set(target, prop, value) { target[prop] = value; return true; } }); proxy.x = 5; // triggers the trap - The
hastrap intercepts theinoperator, useful for hiding certain properties from checks.const proxy = new Proxy({ secret: 1, open: 2 }, { has(target, prop) { return prop === "secret" ? false : prop in target; } }); "secret" in proxy; // false "open" in proxy; // true - The
deletePropertytrap intercepts thedeleteoperator on the object.const proxy = new Proxy({ locked: true }, { deleteProperty(target, prop) { if (prop === "locked") return false; delete target[prop]; return true; } }); delete proxy.locked; // blocked by the trap - The
applytrap intercepts calls when the target itself is a function.function add(a, b) { return a + b; } const traced = new Proxy(add, { apply(target, thisArg, args) { console.log("args:", args); return Reflect.apply(target, thisArg, args); } }); traced(2, 3); // logs "args: [2, 3]", returns 5
Using Reflect for Default Behavior
- Reflect provides a method matching every Proxy trap, like
Reflect.get()andReflect.set().const proxy = new Proxy({ a: 1 }, { get(target, prop, receiver) { return Reflect.get(target, prop, receiver); } }); proxy.a; // 1, identical to default behavior - Pass the
receiverargument through to Reflect methods so getters keep the correctthisbinding.const target = { _value: 10, get value() { return this._value; } }; const proxy = new Proxy(target, { get(target, prop, receiver) { return Reflect.get(target, prop, receiver); } }); proxy.value; // 10, with correct this binding - Reflect methods return clear booleans for success or failure instead of throwing in some cases.
const obj = Object.freeze({ a: 1 }); Reflect.set(obj, "a", 2); // false, no exception thrown - Reflect.ownKeys() and Reflect.deleteProperty() mirror Object methods but work consistently inside traps.
const proxy = new Proxy({ a: 1, b: 2 }, { ownKeys(target) { return Reflect.ownKeys(target); } }); Object.keys(proxy); // ["a", "b"] - Combining Proxy with Reflect keeps trap code minimal — only override what truly needs custom logic.
const handler = { get(target, prop, receiver) { console.log(`get ${String(prop)}`); return Reflect.get(target, prop, receiver); } // other traps fall back to default behavior automatically };
Practical Use Cases
- Use a
settrap to validate data before it's written, rejecting invalid values early.function createValidated(schema) { return new Proxy({}, { set(target, prop, value) { if (schema[prop] && typeof value !== schema[prop]) { throw new TypeError(`${String(prop)} must be ${schema[prop]}`); } return Reflect.set(target, prop, value); } }); } const user = createValidated({ age: "number" }); user.age = "old"; // throws TypeError - Use a
gettrap to log every property access, helpful for debugging unexpected reads.function withLogging(target) { return new Proxy(target, { get(target, prop, receiver) { console.log(`accessed ${String(prop)}`); return Reflect.get(target, prop, receiver); } }); } - Use a
gettrap to supply default values for missing properties instead of returningundefined.function withDefault(target, defaultValue) { return new Proxy(target, { get(target, prop) { return prop in target ? target[prop] : defaultValue; } }); } const counts = withDefault({}, 0); counts.missing; // 0 - Build a simple reactive system by combining a
settrap with a notification callback.function reactive(obj, onChange) { return new Proxy(obj, { set(target, prop, value) { const result = Reflect.set(target, prop, value); onChange(prop, value); return result; } }); } const state = reactive({ count: 0 }, (k, v) => console.log(`${k} -> ${v}`)); state.count = 1; // logs "count -> 1" - Use a Proxy with an
applytrap to wrap functions for timing or retry logic without modifying the original.function withTiming(fn) { return new Proxy(fn, { apply(target, thisArg, args) { const start = performance.now(); const result = Reflect.apply(target, thisArg, args); console.log(`took ${performance.now() - start}ms`); return result; } }); }
Pitfalls to Avoid
- A
gettrap that returns plain functions can break methods relying onthisreferring to the proxy or target.const target = { items: [1, 2, 3], getFirst() { return this.items[0]; } }; const proxy = new Proxy(target, { get(target, prop) { return target[prop]; } // loses receiver binding }); proxy.getFirst(); // works here, but breaks with inherited getters - Traps add overhead to every intercepted operation, so avoid proxies in tight, performance-critical loops.
// Avoid wrapping objects accessed thousands of times per animation frame - A
settrap must returntrue, or strict mode code throws a TypeError on assignment."use strict"; const broken = new Proxy({}, { set() { /* forgot to return true */ } }); broken.a = 1; // throws TypeError in strict mode - Comparing a Proxy to its target with
===returnsfalse, since they are different object identities.const target = {}; const proxy = new Proxy(target, {}); proxy === target; // false - Some traps must satisfy invariants the engine enforces, like not reporting a non-configurable property as deletable.
const target = {}; Object.defineProperty(target, "locked", { configurable: false, value: 1 }); const proxy = new Proxy(target, { deleteProperty() { return true; } // violates invariant, throws TypeError }); delete proxy.locked; // TypeError
Tips
- 01Call the matching Reflect method inside every trap so default behavior still works for properties you don't customize.
- 02Use a Proxy for validation logic, like rejecting invalid property assignments before they ever reach the target object.
- 03Reach for Reflect.has() and Reflect.deleteProperty() over the in and delete operators when writing generic trap handlers.
Warnings
- 01Forgetting to bind this correctly inside a get trap can break methods relying on internal state of the target.
- 02Adding traps to every property access adds real overhead, so avoid wrapping objects accessed in tight performance-critical loops.
- 03A set trap that doesn't return true silently fails in non-strict mode but throws a TypeError in strict mode code.
FAQ
- A Proxy wraps a target object and lets you intercept fundamental operations like reading or writing a property. You define handler functions called traps that run instead of the default behavior. Common uses include validation, logging, default values, and reactive data systems like those in frontend frameworks.
- Proxy intercepts operations on an object through trap functions you define. Reflect is a built-in object with methods that perform the same default operations, like Reflect.get() or Reflect.set(). They work together — Reflect calls the original behavior from inside a trap.
- Reflect methods correctly forward the receiver and preserve this binding, which matters for inherited properties and getters. Using target[key] directly can break in subtle cases involving prototype chains. Reflect also returns consistent boolean success values for operations like set and deleteProperty.
- get and set intercept property reads and writes, the two most common traps. has intercepts the in operator, and deleteProperty intercepts delete. apply intercepts function calls when the target is itself a function. Each trap receives the target, the property or arguments, and sometimes the proxy itself as the receiver.
- Yes, noticeably compared to plain property access, since every trapped operation runs through a function call. The overhead is usually fine for configuration objects, API wrappers, or validation layers. Avoid proxies on objects accessed thousands of times per frame in hot loops or animations.