The Proxy and Reflect API — intercepting what happens to an object
Every other way of adding behavior to an object requires modifying that object directly — a getter, a method, a class. Proxy is genuinely different: it wraps an EXISTING object and lets you intercept fundamental operations (reading a property, setting one, checking existence) on it, without changing the original object at all.
3 min read
What a Proxy actually does: a wrapper that intercepts fundamental operations
const target = { name: "Ada" };
const proxy = new Proxy(target, {
get(obj, prop) {
console.log(`Reading property: ${prop}`);
return obj[prop];
},
});
proxy.name; // logs "Reading property: name", then returns "Ada"new Proxy(target, handler) creates a new object that behaves like target for every operation except the ones the handler object defines a trap for — here, get intercepts every property read, running custom logic (logging, in this case) before returning the actual value. This is a genuinely different mechanism from @property-style getters (covered in this platform's Python domain), which only intercept one specific, named property — a Proxy's get trap intercepts every property read on the object, known or unknown, without needing to declare each one individually.
The most practically useful trap: set, for real, live validation
const validated = new Proxy({}, {
set(obj, prop, value) {
if (prop === "age" && (typeof value !== "number" || value < 0)) {
throw new TypeError("age must be a non-negative number");
}
obj[prop] = value;
return true; // MUST return true to indicate the set succeeded — required by the trap's contract
},
});
validated.age = 30; // works fine
validated.age = -5; // throws IMMEDIATELY — validation runs on EVERY assignment, automaticallyThe set trap intercepts every property assignment, letting validation logic run automatically on any property being set, without needing a separate setter method written for each individual property the way @property-based validation would require one setter per field. This is a real, practical use: enforcing invariants on a plain object generically, rather than needing a full class with individually hand-written setters for every validated field.
Reflect: the "default behavior" companion, used correctly inside a trap
const logged = new Proxy(target, {
get(obj, prop, receiver) {
console.log(`Reading: ${prop}`);
return Reflect.get(obj, prop, receiver); // the CORRECT way to perform the default get — not obj[prop]
},
});Reflect provides functions that mirror exactly what a Proxy trap would do by default (Reflect.get, Reflect.set, and so on) — using Reflect.get(obj, prop, receiver) instead of plain obj[prop] inside a get trap correctly preserves subtler behaviors (like the right this binding when the target has its own getters) that a naive obj[prop] can silently get wrong in edge cases. The practical pattern: Proxy intercepts an operation, and Reflect's matching function is the correct, standard way to actually perform that operation's real, default behavior from inside the trap when the trap isn't rejecting or transforming it.
Where this actually shows up in real, production code
// A SIMPLIFIED version of what a reactive framework's state-tracking does
function reactive(obj) {
return new Proxy(obj, {
set(target, key, value) {
target[key] = value;
triggerRerender(); // automatically detects that SOMETHING changed, without manual tracking
return true;
},
});
}This is genuinely how some reactive UI frameworks (a simplified version of what Vue 3's reactivity system does internally) detect when application state changes, without requiring a developer to manually call something like setState() — wrapping a plain object in a Proxy with a set trap means any direct property assignment (state.count = 5) is automatically observable, triggering a re-render or other reactive update, entirely transparently to code that just does ordinary property assignment.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. How does a Proxy's `get` trap differ from a property-specific getter (like Python's @property)?
2. What does the `set` trap on a Proxy enable that would otherwise require a class with hand-written setters?
3. Why is `Reflect.get(obj, prop, receiver)` the correct way to perform default behavior inside a Proxy trap, rather than plain `obj[prop]`?