JavaScript’s Proxy and Reflect APIs are among the most powerful metaprogramming tools available in the language. They allow developers to intercept and customize fundamental operations on objects — property access, assignment, enumeration, function invocation, and even constructor calls. This article explores all 13 proxy traps, the complementary Reflect API, and real-world patterns used in production frameworks like Vue 3, MobX, and Immer.
Understanding the Proxy Pattern
A Proxy wraps a target object and intercepts operations via handler functions called traps:
const target = { name: "Alice", age: 30 };
const handler = {
get(target, prop, receiver) {
console.log(`Getting property "${prop}"`);
return Reflect.get(target, prop, receiver);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // Logs: Getting property "name" → "Alice"
The target remains unmodified. The handler defines how operations on the proxy are forwarded, modified, or blocked. Proxy differs from wrapper functions or decorators because it intercepts at the language level — in, delete, for...in, and new can all be customized.
The 13 Proxy Traps
Proxy provides 13 traps that cover every fundamental object operation. The most commonly used are get and set, which intercept property reading and writing. Here is a complete reference:
| Trap | Fires When | Arguments |
|---|---|---|
get | Property read | target, prop, receiver |
set | Property write | target, prop, value, receiver |
has | in operator | target, prop |
deleteProperty | delete operator | target, prop |
ownKeys | Object.keys(), for...in | target |
apply | Function call | target, thisArg, args |
construct | new keyword | target, args, newTarget |
getPrototypeOf | Object.getPrototypeOf | target |
setPrototypeOf | Object.setPrototypeOf | target, proto |
isExtensible | Object.isExtensible | target |
preventExtensions | Object.preventExtensions | target |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor | target, prop |
defineProperty | Object.defineProperty | target, prop, desc |
Each trap must enforce invariants. For example, a get trap cannot return a value different from the actual value for a non-writable, non-configurable property.
The Reflect API
Reflect methods mirror every proxy trap and provide the default forwarding behavior. Using Reflect inside traps ensures correct propagation of the receiver argument, which is critical for proper this binding in inherited properties and getters:
const parent = {
get fullName() { return `${this.first} ${this.last}`; }
};
const handler = {
get(target, prop, receiver) {
// Without Reflect.get, the getter's 'this' would be wrong
return Reflect.get(target, prop, receiver);
}
};
const proxy = new Proxy(Object.create(parent), handler);
proxy.first = "John";
proxy.last = "Doe";
console.log(proxy.fullName); // "John Doe"
The mental model is simple: Reflect is to Proxy what Object is to direct property access. Always forward traps through Reflect to maintain default behavior while adding custom logic.
Validation and Data Integrity
The set trap can enforce schema constraints on objects:
const validator = {
set(target, prop, value) {
if (prop === "age" && (!Number.isInteger(value) || value < 0)) {
throw new TypeError("Age must be a positive integer");
}
if (prop === "email" && !/^[^\s@]+@[^\s@]+$/.test(value)) {
throw new TypeError("Invalid email format");
}
return Reflect.set(target, prop, value);
}
};
const user = new Proxy({}, validator);
user.age = 25; // OK
user.age = -5; // TypeError: Age must be a positive integer
A read-only proxy blocks all mutation traps:
const readOnly = target =>
new Proxy(target, {
set: () => { throw new Error("Object is read-only"); },
deleteProperty: () => { throw new Error("Object is read-only"); },
defineProperty: () => { throw new Error("Object is read-only"); },
});
Virtual Properties and Computed Values
Proxy can expose properties that do not exist on the target object by computing them dynamically in the get trap:
const range = (from, to) =>
new Proxy({ from, to }, {
get(target, prop) {
if (prop === "size") return target.to - target.from + 1;
if (prop === "contains") return (v) => v >= target.from && v <= target.to;
return Reflect.get(target, prop);
}
});
const r = range(1, 10);
console.log(r.size); // 10
console.log(r.contains(5)); // true
This lazy evaluation pattern can be extended with caching via WeakMap to avoid recomputation.
Property Observation and Change Tracking
A simple observable using the set trap:
function observe(target, onChange) {
return new Proxy(target, {
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver);
onChange(prop, value);
return result;
}
});
}
For deep observation, wrap nested objects recursively with WeakMap caching to avoid double-wrapping. This technique is the foundation of Vue 3’s reactive() system, which replaced Vue 2’s Object.defineProperty-based approach.
Revocable Proxies
Proxy.revocable creates a proxy that can be permanently disabled:
const { proxy, revoke } = Proxy.revocable(target, handler);
// use proxy normally...
revoke();
// any subsequent operation throws TypeError
This is useful for granting temporary access to sensitive resources and enforcing cleanup contracts.
Production Patterns: Vue 3, MobX, Immer
Vue 3 uses Proxy in its reactive() system. The get trap tracks property access to build dependency graphs, and set triggers re-renders. MobX 6+ similarly replaced defineProperty with Proxy for observable(). Immer.js uses Proxy to produce immutable drafts with copy-on-write semantics — set marks modifications, and get creates nested drafts on access.
Performance Considerations
Each trapped operation carries overhead. Avoid proxying hot-path objects in tight loops. Apply proxies selectively — “proxy inside, not outside” — and profile before optimizing. Deep proxies with large WeakMap caches can also impact memory usage.
Proxy is the right tool for validation, reactivity, and virtual resources. For performance-critical paths, consider whether a simpler pattern suffices. When applied judiciously, Proxy enables metaprogramming patterns that would be impossible or cumbersome with any other JavaScript feature.
