Proxy & Reflect
A Proxy wraps an object and lets you intercept the fundamental operations performed on it — reading a property, writing one, checking existence, deleting, and more. Each interception is called a trap. The Reflect API is its natural companion: a collection of static methods that perform those same fundamental operations as plain functions, giving you a clean way to invoke the default behavior from inside a trap. Together they power validation layers, reactive frameworks like Vue 3, smart defaults, and transparent logging — all without touching the original object.
Creating a proxy
A proxy is built from two arguments: the target (the object being wrapped) and a handler (an object whose methods are the traps). Operations on the proxy are forwarded to the matching trap; any operation without a trap falls through to the target unchanged.
const target = { message: "hello" };
const proxy = new Proxy(target, {
get(obj, prop, receiver) {
return prop in obj ? obj[prop] : `<no ${String(prop)}>`;
},
});
console.log(proxy.message);
console.log(proxy.missing);
Output:
hello
<no missing>
The proxy is a transparent stand-in: from the outside it behaves like the target, but every access flows through your handler first.
Common traps
Each trap mirrors a low-level object operation. The most-used ones:
| Trap | Triggered by | Signature |
|---|---|---|
get | reading obj.x, obj["x"] | (target, prop, receiver) |
set | writing obj.x = v | (target, prop, value, receiver) |
has | "x" in obj | (target, prop) |
deleteProperty | delete obj.x | (target, prop) |
ownKeys | Object.keys, for...in | (target) |
apply | calling a function proxy | (target, thisArg, args) |
construct | new on a function proxy | (target, args, newTarget) |
The set trap must return a truthy value to signal success; returning false (or nothing) throws a TypeError in strict mode. The has trap controls what the in operator reports.
const config = new Proxy({ debug: false }, {
has(target, prop) {
// Hide any property that starts with an underscore.
if (typeof prop === "string" && prop.startsWith("_")) return false;
return prop in target;
},
deleteProperty(target, prop) {
if (prop === "debug") throw new Error("debug cannot be removed");
delete target[prop];
return true;
},
});
config._secret = 42;
console.log("_secret" in config);
console.log("debug" in config);
Output:
false
true
Reflect: the default-behavior toolkit
Inside a trap you often want to do the normal thing — but with a hook around it. Writing target[prop] = value works for simple cases, yet it ignores the receiver and can break inheritance with getters/setters. Reflect solves this: every trap has a one-to-one Reflect method with the same arguments, so forwarding is trivial and correct.
| Reflect method | Equivalent operation |
|---|---|
Reflect.get(t, k, r) | t[k] |
Reflect.set(t, k, v, r) | t[k] = v |
Reflect.has(t, k) | k in t |
Reflect.deleteProperty(t, k) | delete t[k] |
Reflect.ownKeys(t) | Object.keys + symbols |
Forwarding the receiver matters when the proxy sits on a prototype and a getter references this. Reflect.get(target, prop, receiver) ensures this points at the proxy, not the raw target.
Always prefer
Reflect.set(target, prop, value, receiver)overtarget[prop] = valueinside asettrap. TheReflectform preserves the correctreceiverand returns the boolean success flag your trap is required to return.
Use case: validation
Proxies make great validation gates because they intercept assignment before it lands.
function validatedUser(initial = {}) {
return new Proxy(initial, {
set(target, prop, value, receiver) {
if (prop === "age" && (!Number.isInteger(value) || value < 0)) {
throw new TypeError("age must be a non-negative integer");
}
if (prop === "email" && !String(value).includes("@")) {
throw new TypeError("email must contain @");
}
return Reflect.set(target, prop, value, receiver);
},
});
}
const user = validatedUser();
user.age = 30;
console.log(user.age);
try {
user.age = -5;
} catch (err) {
console.log(err.message);
}
Output:
30
age must be a non-negative integer
Use case: reactivity and logging
Vue 3’s reactivity system is built on proxies: a get trap records which effect read a property (dependency tracking), and a set trap re-runs those effects. A simplified version shows the shape of the idea.
function reactive(obj, onChange) {
return new Proxy(obj, {
get(target, prop, receiver) {
console.log(`read ${String(prop)}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
const ok = Reflect.set(target, prop, value, receiver);
onChange(prop, value);
return ok;
},
});
}
const state = reactive({ count: 0 }, (prop, value) =>
console.log(`changed ${prop} -> ${value}`)
);
state.count;
state.count = 1;
Output:
read count
changed count -> 1
The same get/set logging pattern is invaluable for debugging — drop a proxy around any object and watch every interaction without editing the code that uses it.
Use case: smart defaults
A get trap can synthesize values for missing keys, turning a plain object into a defaulting dictionary.
const counts = new Proxy({}, {
get: (target, prop) => (prop in target ? target[prop] : 0),
});
console.log(counts.apples);
counts.apples = 3;
console.log(counts.apples);
Output:
0
3
Revocable proxies
Proxy.revocable(target, handler) returns { proxy, revoke }. After calling revoke(), any operation on the proxy throws — useful for handing out access that you can later cut off (e.g. tokens or sandboxed APIs).
Best practices
- Use
Reflectmethods inside traps to invoke default behavior; never hand-roll operations that drop thereceiver. - Return the correct value from each trap —
set,deleteProperty, anddefinePropertymust return a boolean, or strict mode throws. - Keep traps fast and side-effect-light; they run on every matching operation and can hide expensive work behind innocent-looking property access.
- Respect proxy invariants — you cannot, for example, report a non-configurable, non-writable property as a different value, or the engine throws a
TypeError. - Reach for
Proxy.revocablewhen you need to invalidate access cleanly rather than leaking a live reference. - Avoid proxying performance-critical hot paths; the indirection has real overhead compared to direct property access.