JavaScript Proxy & Reflect: Guard Pattern
How does a modern framework just know when your state changed? You write state.count++ — no setter call, no event — and the UI updates. The…
JavaScript Proxy & Reflect: Guard Pattern
How does a modern framework just know when your state changed? You write state.count++ — no setter call, no event — and the UI updates. The answer, in Vue 3, MobX, and SolidJS alike, is Proxy: a JavaScript feature most of us use every day through libraries but have never written directly.
This article fixes that: what Proxy actually does, where it earns its keep in real applications, and why Reflect — the API everyone ignores — is the difference between a proxy that works and one that silently breaks.

What is a Proxy?
A Proxy wraps an object and intercepts operations on it — property reads, writes, deletions, even function calls. The interceptors are called traps.
const user = { name: "Sandeep" };
const proxy = new Proxy(user, {
get(target, prop) {
console.log(`Reading ${prop}`);
return target[prop];
},
set(target, prop, value) {
console.log(`Setting ${prop} = ${value}`);
target[prop] = value;
return true; // must return true on success
}
});
proxy.name; // logs "Reading name" → "Sandeep"
proxy.age = 30; // logs "Setting age = 30"
The caller doesn’t know a proxy exists. That’s the whole point: behavior happens automatically on access or change, without callers doing anything special.
There are 13 traps in total. The ones you’ll actually use: get, set, has (the in operator), deleteProperty, apply (function calls), construct (new), and ownKeys (Object.keys, spread).
Where Proxy Shows Up in Real Applications
1. Framework reactivity (the big one)
Vue 3, MobX, and SolidJS use Proxy to detect state changes and re-render the UI:
const state = new Proxy(data, {
set(target, prop, value) {
target[prop] = value;
reRenderComponent(); // framework triggers UI update
return true;
}
});
You write state.count++, the UI updates. No setState, no dirty checking. That's a set trap firing.
2. Dynamic API clients
Build an SDK without defining a single method:
const api = new Proxy({}, {
get(_, endpoint) {
return (params) =>
fetch(`/api/${endpoint}`, { body: JSON.stringify(params) });
}
});
api.getUsers({ id: 1 }); // → calls /api/getUsers
api.createOrder({...}); // → never defined anywhere, still works
ORMs like Prisma-style query builders and test-mocking libraries use the same trick — intercepting arbitrary property access to build queries or mocks on demand.
3. Validation at the source
Reject bad data at assignment time instead of scattering checks everywhere:
set(target, prop, value) {
if (prop === "age" && value < 0) throw new Error("Invalid age");
target[prop] = value;
return true;
}
4. Logging, audit trails, and access control
Ever spent an afternoon hunting down what mutated a config object? Wrap it in a proxy and every change identifies itself:
const config = new Proxy(rawConfig, {
set(target, prop, value) {
console.trace(`config.${prop} changed to ${value}`); // full stack trace
target[prop] = value;
return true;
}
});
The same idea powers access control — hide sensitive fields so they don’t leak through Object.keys, spread, or JSON.stringify:
ownKeys(target) {
return Reflect.ownKeys(target).filter(k => !k.startsWith("_"));
}
Combine the two and you have an audit layer that costs callers nothing: they keep writing config.retries = 5, and you get a paper trail for free.
Enter Reflect: The Sidekick
Start with the simple truth: Reflect is just “do the normal thing” as a function.
obj.name // normal way
Reflect.get(obj, "name") // exact same thing, as a function call
obj.name = "x" // normal way
Reflect.set(obj, "name", "x") // same thing, as a function call
That’s all Reflect is — a toolbox where every object operation exists as a plain function, with one method matching every Proxy trap. Which raises the obvious question: why call Reflect.get(obj, "name") when obj.name exists?
Because inside a proxy trap, there’s one situation where “the normal way” gives the wrong answer. Here it is, with visible wrong output:
// A base object with a getter that uses `this`
const user = {
_name: "Guest",
get name() { return this._name; }
};
const proxy = new Proxy(user, {
get(target, prop) {
return target[prop]; // "do it by hand"
}
});
// Another object INHERITS from the proxy
const admin = Object.create(proxy);
admin._name = "Sandeep"; // admin has its own _name
console.log(admin.name); // ❓
admin has its own _name = "Sandeep", so admin.name should be "Sandeep".
It prints "Guest". Follow the chain:
admin.name→ admin has nonameproperty of its own- JS walks up the prototype chain → finds the proxy → the
gettrap fires - The trap runs
target[prop]→ that'suser.name→ the getter runs with**this = user** - The getter returns
user._name→"Guest"❌
The bug: by the time we’re inside the trap, we’ve lost track of who originally asked. The question came from admin, but target[prop] answers it as if user asked.
That’s exactly what the third trap argument — receiver — is for. It remembers who originally asked. And Reflect.get is the only way to pass it along:
const proxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin, the original asker
return Reflect.get(target, prop, receiver);
}
});
console.log(admin.name); // "Sandeep" ✅ — getter ran with this = admin
Reflect.get(target, prop, receiver) means: "read this property the normal way, but if a getter runs, set this to the original asker." There's no syntax for that — target[prop] can't do it. Only the function form can.
One-sentence summary: **target[prop] answers the question as the wrong person; Reflect.get(..., receiver) answers it as whoever actually asked.** In 95% of cases they behave identically — Reflect is insurance for the 5% (getters + inheritance) where they don't. Vue 3's source code uses Reflect.get/Reflect.set everywhere for exactly this reason.
Reflect has a second selling point — it fails gracefully where language syntax throws:
delete obj.frozen; // TypeError in strict mode 💥
Reflect.deleteProperty(obj, "frozen"); // false — handle it calmly
Reflect.set(frozenObj, "a", 1); // false, instead of silent failure
The Security Guard Metaphor
Here’s the mental model that makes it all click.
Proxy is a security guard at a building door. Every request — “read this property”, “write that value” — must pass through it. The guard decides: allow, block, log, modify.
Reflect is the building’s actual door mechanism. Once the guard approves you, the door still has to open the proper way.
A guard who kicks the door open himself (target[prop] = value) usually gets away with it — but he doesn't know about the building's special mechanisms: setters, inheritance, this-binding, frozen properties. Reflect does.
So the canonical pattern in virtually every production proxy handler:
set(target, prop, value, receiver) {
if (prop === "role" && !isAdmin()) return false; // your guardrail
return Reflect.set(target, prop, value, receiver); // safe default
}
Your custom code handles the 5% you care about; Reflect handles the 95% of language semantics you don’t want to reimplement by hand.
Bonus: Proxy.revocable — The Kill Switch
Need to hand out temporary access to an object? Proxy.revocable gives you a proxy plus a revoke function:
const { proxy, revoke } = Proxy.revocable(userData, {});
proxy.name; // works
revoke();
proxy.name; // TypeError: proxy has been revoked
After revoke(), every operation throws, permanently. The proxy also drops its internal reference to the target, so the target can be garbage collected.
Real use — lend an object for a limited time:
function lendObject(obj, ms) {
const { proxy, revoke } = Proxy.revocable(obj, {});
setTimeout(revoke, ms);
return proxy;
}
const session = lendObject(userData, 60_000); // valid for 1 minute
Sandboxing frameworks take this further with the membrane pattern: wrap every object crossing a trust boundary in a revocable proxy, keep all the revoke functions, and sever the entire object graph with one call.
Gotchas Worth Knowing
- Private fields (
#x) and internal slots don't work through proxies. Wrapping aMapbreaks its methods. Fix: bind methods to the target in thegettrap. - Identity:
proxy !== target. If code mixes raw objects and proxies as Map/Set keys, lookups fail. Vue caches proxies in aWeakMapso the same target always gets the same proxy. - Performance: trapped operations are slower than plain property access. Fine for state management; avoid proxying hot-loop data.
- Deep reactivity isn’t free: a
gettrap only fires at the top level. Frameworks lazily wrap nested objects on read:
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
return typeof value === "object" && value !== null
? reactive(value) // wrap on access
: value;
}
That’s the core of Vue 3’s reactive() in about ten lines.
Takeaway
Proxy lets you add rules on top of an object — validation, logging, reactivity, access control — without callers ever knowing. Reflect makes sure that after your rules run, the object still behaves exactly like JavaScript intends.
Guard first, Reflect last. That one pattern covers nearly everything you'll ever do with these two APIs.
메타데이터
- post_id
- 70f00d796a0d
- slug
- javascript-proxy-reflect-guard-pattern-70f00d796a0d
- url
- https://medium.com/tutorial-savvy/javascript-proxy-reflect-guard-pattern-70f00d796a0d
- canonical_url
- https://medium.com/tutorial-savvy/javascript-proxy-reflect-guard-pattern-70f00d796a0d
- author_url
- https://medium.com/@tutorialsavvy
- status
- ok
- fetched_at
- 2026-08-18 05:42:08