
Learn how JavaScript Proxy and Reflect intercept object operations: the 13 traps and why Reflect matters.
Martin Ferret
September 8, 2026
Mutate a reactive() object in Vue 3 and the DOM updates on its own. No compiler step, no polling. Something intercepted the assignment, and that something is a Proxy.
A Proxy is a stand-in that you place in front of an object. Code talks to the stand-in, the stand-in decides what to do, and usually forwards to the real object.
You define what to intercept with a handler. Each handler method is called a trap.
const user = new Proxy({ name: 'Ada' }, {
get(target, key) {
console.log(`read: ${String(key)}`);
return target[key];
}
});
user.name; // logs "read: name", returns 'Ada'
Two things matter here.
First, anything you do not trap keeps its normal behaviour. new Proxy(target, {}) acts almost exactly like the target, so you intercept one operation at a time rather than reimplementing an object.
Second, there are thirteen traps, one per fundamental operation on an object. You will realistically use five: get for reads, set for writes, has for in, deleteProperty for delete, and apply for calling a proxied function.
The example above forwards with target[key]. That is the natural instinct, and it is subtly wrong.
Consider a target with a getter:
const target = {
_name: 'Ada',
get name() {
return this._name;
}
};
Read proxy.name and the trap fires once. Then the getter runs, and this is the raw target, not the proxy. The _name lookup happens behind your back. Your interception has a hole in it, and in a reactivity system that hole is a dependency you failed to track.
Reflect closes it. It is a companion object with one static method per trap, taking the same arguments. Its third argument, the receiver, sets this inside any getter.
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
}
Now the getter runs with this bound to the proxy, so _name goes through the trap too.
Reflect also gets return values right. The set trap must return a boolean saying whether the write succeeded, and in strict mode, which means in every module, returning false throws a TypeError. Reflect.set() returns exactly that boolean, where a handwritten return true would sometimes lie.
The rule to remember: inside a trap, forward with the matching Reflect method.
The interesting uses are the ones you cannot express with a getter or a setter, because they apply to keys you do not know in advance.
Reactivity is the clearest example. Vue wraps your state in a Proxy, records which effect read which key in the get trap, and re-runs those effects from the set trap.
js
function reactive(object) {
return new Proxy(object, {
get(target, key, receiver) {
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key);
return result;
}
});
}
That is the whole idea. track and trigger are bookkeeping, a map from object to key to a set of effects. The mechanism is these two traps.
This is also why Vue 2 needed Vue.set() and Vue 3 does not. Object.defineProperty could only instrument properties that already existed, while a Proxy also intercepts keys added later.
The same shape covers validation on write, access control, logging, lazy loading and test stubs. Always the same pattern: intercept, do your work, forward with Reflect.
A Proxy is not allowed to lie about everything. If the target has a property that is non writable and non configurable, the get trap must return its real value.
js
const frozen = Object.freeze({ id: 42 });
new Proxy(frozen, { get: () => 'nope' }).id; // TypeError
These rules are called invariants, and the engine enforces them so a frozen object stays trustworthy behind a wrapper.
Two other limits show up in practice, both for the same underlying reason: a method called through a proxy receives the proxy as this, and some data only exists on the real object.
Private class fields are one case. #count lives in the instance, so a method invoked on a proxied instance throws a TypeError.
Built ins are the other. Map, Set, Date and typed arrays store their contents in internal slots, and a proxy has none.
js
new Proxy(new Map(), {}).set('a', 1); // TypeError
Both are fixable by binding methods back to the target in the get trap, but it is better to know the limit than to discover it in production.
Use a Proxy for cross cutting concerns that apply to unknown keys. Prefer a getter, a setter or a plain function when they say the same thing, because code that behaves differently from how it reads is hard to debug, and a Proxy is exactly that by design.
Two practical notes. A trapped operation costs a function call plus a Reflect dispatch, so it is far slower than a plain property access. Wrap a boundary, not the array you iterate ten thousand times. And Proxy cannot be polyfilled, since its behaviour is unobservable from user land JavaScript. That is why Vue 3 dropped Internet Explorer outright.
Understanding these two objects is what turns "the framework re-renders somehow" into "the get trap tracked my dependency".
Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

Type-Safe Server Routes
Type-Safe Server Routes: End-to-End Types from server/api to Your Components How Nuxt infers response types from your server routes so useFetch and $fetch calls are fully typed without manual interfaces.
Reza Baar
Sep 16, 2026

How to create nested routes with Angular?
Learn how Angular nested routes and child routes work with multiple router outlets, and see how to use them to build navigable sections such as tabbed dashboards.
Alain Chautard
Sep 15, 2026

Structuring a Large Vue App: A Feature-Based Folder Architecture
Why the default components/composables/stores split breaks down at scale, and how a feature-first structure fixes it.
Reza Baar
Sep 9, 2026