This article is published in English.
Understanding JavaScript Proxy: Traps, Reflect, and Reactive Patterns
Learn how JavaScript's Proxy and Reflect objects intercept property access to power validation, virtual properties, and reactive frameworks.
Every time you write obj.property or assign to it with obj.property = value, you are leaning on operations that JavaScript carries out quietly, with no built-in way to watch or alter what happens underneath. The Proxy object removes that assumption entirely. It lets you wrap a target object and intercept these fundamental operations, reading, writing, deleting, before they take effect. That interception mechanism turns out to be the real engine behind a lot of what looks like "magic" in modern JavaScript libraries and tools.
A Wrapper That Can Intercept Anything
A Proxy sits between your code and the object it wraps, and a collection of trap functions decide what actually happens for each kind of operation performed on it.
const config = { retries: 3, timeout: 5000 };
const guardedConfig = new Proxy(config, {
set(target, key, value) {
if (key === "retries" && (typeof value !== "number" || value < 0)) {
throw new TypeError("retries must be a non-negative number");
}
target[key] = value;
return true;
},
});
guardedConfig.retries = 5; // works fine
guardedConfig.retries = -1; // throws TypeError, caught before it ever reaches the object
Nothing about writing guardedConfig.retries = 5 looks any different from an ordinary property assignment. That is exactly the point of the design: the interception stays invisible at the call site. You can layer in validation, logging, or other side effects on top of what looks like plain property access, without forcing any change on the code that actually uses the object.
The Mechanism Behind Reactive Frameworks
function reactive(target, onChange) {
return new Proxy(target, {
get(obj, key) {
return obj[key];
},
set(obj, key, value) {
const changed = obj[key] !== value;
obj[key] = value;
if (changed) onChange(key, value);
return true;
},
});
}
const state = reactive({ count: 0 }, (key, value) => {
console.log(`${key} changed to ${value}, re-rendering...`);
});
state.count = 1; // "count changed to 1, re-rendering..." — no explicit call needed
Writing state.count = 1 reads like a completely normal assignment, because syntactically it is one. What makes it meaningful is the set trap, which converts that unremarkable line into an event the rest of the system can respond to. This is the real foundation reactive frameworks rely on. There is no special "observable" data type you need to opt into everywhere. It is plain objects wrapped in a Proxy that silently detects every write made to them.
Virtual Properties: Values That Exist Only When You Ask
A get trap is free to return something that was never actually stored on the underlying target, which means you can expose computed or derived values as though they were ordinary properties.
const product = { name: "Desk Lamp", priceCents: 3499 };
const productView = new Proxy(product, {
get(target, key) {
if (key === "priceDollars") {
return (target.priceCents / 100).toFixed(2);
}
return target[key];
},
});
console.log(productView.priceDollars); // "34.99" — computed on read, not stored anywhere
There is no priceDollars field anywhere on product. It gets produced on the fly, each time it's accessed, inside the get trap. This is meaningfully different from a getter you'd write with get inside a class or object literal, because a Proxy trap can intercept access to keys that were never declared ahead of time at all. That's useful, for example, when you want to wrap an API response with extra computed fields without touching the original payload.
Why Reflect Tends to Appear Alongside Proxy
There's a subtlety that catches people the first time they write a trap that needs to fall back to default behavior. Doing it the naive way is easy to get subtly wrong.
const handler = {
get(target, key) {
console.log(`reading ${key}`);
return target[key]; // works, but loses some edge-case correctness
},
};
For simple, plain objects this approach mostly holds up, but the safer and more correct way to forward an operation to its default behavior is through Reflect. It exposes a function for every trap that mirrors the operation exactly, preserving the right this binding and handling trickier situations, like inherited getters, correctly.
const handler = {
get(target, key, receiver) {
console.log(`reading ${key}`);
return Reflect.get(target, key, receiver);
},
};
Calling Reflect.get(target, key, receiver) reproduces exactly what ordinary property access would have done, including edge cases involving prototypes and getters that depend on this, situations where target[key] can quietly produce the wrong result. In practical Proxy code, the convention is almost always to call the matching Reflect method from inside each trap, unless you are intentionally changing the default behavior, since it's the version guaranteed to line up with what plain property access would normally do.
Traps That Restrict Rather Than Extend
Traps aren't only useful for adding behavior, they can just as easily take it away. A has trap governs what the in operator reports back, and a deleteProperty trap can refuse deletion altogether.
const secureRecord = new Proxy(
{ id: 1, ssn: "123-45-6789" },
{
get(target, key) {
if (key === "ssn") throw new Error("Direct access to ssn is not allowed");
return Reflect.get(target, key);
},
deleteProperty() {
throw new Error("Deleting fields is not allowed on this record");
},
}
);
console.log(secureRecord.id); // 1
console.log(secureRecord.ssn); // throws
delete secureRecord.id; // throws
This kind of protection differs in an important way from a private class field: it can be applied to any object at all, with no need for that object to have been written as a class to begin with, and it lets you define narrow, per-key rules that a single blanket #private boundary simply cannot express.
One Idea, Many Traps
Every trap is really answering the same question: what should happen the instant code tries to perform this everyday operation on this object? Reading a value, writing one, deleting one, checking whether a key exists, these are all normally invisible, automatic actions, and Proxy is what turns each one into a moment you can observe or redirect. Reactive state systems, validation wrappers, access control layers, virtual computed fields, none of these are unrelated tricks pulling from different bags. They are all built on the identical underlying mechanism, simply pointed at a different trap.