Proxy and Reflect
A Proxy lets you wrap an object and intercept the fundamental operations performed on it — reading a property, writing one, deleting one, checking with in, even calling it as a function. Reflect is its companion: a set of methods that perform those same operations on a plain object, so the inside of a proxy handler can do "the default thing" without trying to recreate it by hand.
The shape of a Proxy
const target = { name: "Ada", age: 36 };
const handler = {
get(obj, prop) {
console.log("read", prop);
return obj[prop];
},
set(obj, prop, value) {
console.log("write", prop, "=", value);
obj[prop] = value;
return true;
},
};
const p = new Proxy(target, handler);
p.name; // logs: read name -> "Ada"
p.age = 37; // logs: write age = 37
p.name; // logs: read name -> "Ada"
console.log(target.age); // 37A handler is just an object of optional trap methods. Any trap you do not provide falls through to the default behaviour on the target.
The common traps
get(target, prop, receiver)— reading a property.set(target, prop, value, receiver)— assigning a property. Must returntruefor success.has(target, prop)— theinoperator.deleteProperty(target, prop)— thedeleteoperator.ownKeys(target)— whatObject.keys,for..inand spread see.getOwnPropertyDescriptor,defineProperty— fine-grained property control.apply(target, thisArg, args)— calling the proxy as a function.construct(target, args)— calling it withnew.
Reflect — the matched API
Reflect mirrors every trap with a function that performs the default operation. Inside a handler, calling Reflect.get(target, prop, receiver) does exactly what would have happened without your proxy — useful for "intercept and forward" patterns.
const logged = new Proxy({ x: 1 }, {
get(target, prop, receiver) {
console.log("get", prop);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log("set", prop, "=", value);
return Reflect.set(target, prop, value, receiver);
},
});
logged.x; // get x -> 1
logged.x = 2; // set x = 2The handler logs, then forwards. Without Reflect, you would manually assign target[prop] = value and remember to return true — Reflect does both.
Use case 1 — validation
Block bad writes at the boundary.
function validatedUser(initial) {
return new Proxy(initial, {
set(target, prop, value) {
if (prop === "age" && (typeof value !== "number" || value < 0)) {
throw new TypeError("age must be a non-negative number");
}
if (prop === "email" && !String(value).includes("@")) {
throw new TypeError("email must contain @");
}
return Reflect.set(target, prop, value);
},
});
}
const user = validatedUser({ name: "Ada", age: 36 });
user.age = 37; // ok
// user.age = -1; // TypeErrorUse case 2 — observables
Notify subscribers whenever a property changes. This is the trick behind Vue's reactivity system.
function observable(target, onChange) {
return new Proxy(target, {
set(obj, prop, value) {
const ok = Reflect.set(obj, prop, value);
onChange(prop, value);
return ok;
},
});
}
const state = observable(
{ theme: "light", lang: "en" },
(key, val) => console.log("changed:", key, val),
);
state.theme = "dark"; // logs: changed: theme darkUse case 3 — defaults and missing-key handling
const withDefault = (obj, fallback) =>
new Proxy(obj, {
get(target, prop) {
return prop in target ? target[prop] : fallback;
},
});
const colors = withDefault({ red: "#f00", blue: "#00f" }, "#888");
colors.red; // "#f00"
colors.purple; // "#888"Use case 4 — debug logs
function trace(name, obj) {
return new Proxy(obj, {
get(t, p) {
console.log(name + "." + String(p) + " read");
return Reflect.get(t, p);
},
});
}
const cart = trace("cart", { items: [], total: 0 });
cart.items;
cart.total;cart.items read cart.total read
Pitfalls and limits
Proxies are not transparent to
===. The proxy is a different object from its target.Some built-ins (
Map,Set,Date,Promise) rely on internal slots and do not work through a proxy without extra handling — the trap returns the right callable, butthismust be re-bound to the target.A proxy adds a per-access cost. Fine for app-level state, expensive for inner loops.
You cannot proxy a primitive. Wrap it in an object first.