JavaScript — Proxies & Reflect
The Proxy object enables you to intercept and customize fundamental operations on another object — property lookup, assignment, enumeration, function invocation, and more. Proxies create a layer of indirection that can trap operations and redefine their behavior without modifying the target object itself.
Introduced in ES2015, proxies pair naturally with the Reflect API, which provides methods that mirror each proxy trap. Using Reflect inside traps ensures default behavior is preserved while allowing custom logic to run before or after.
This page covers every proxy trap, the Reflect API, common use cases like validation and caching, and performance considerations.
| 1 | // A Proxy wraps a target object and traps operations |
| 2 | const target = { message: "hello" }; |
| 3 | |
| 4 | const handler = { |
| 5 | get(target, property, receiver) { |
| 6 | console.log(`Getting property: ${property}`); |
| 7 | return Reflect.get(target, property, receiver); |
| 8 | } |
| 9 | }; |
| 10 | |
| 11 | const proxy = new Proxy(target, handler); |
| 12 | |
| 13 | console.log(proxy.message); |
| 14 | // Getting property: message |
| 15 | // "hello" |
| 16 | |
| 17 | // The original target remains unmodified |
| 18 | console.log(target.message); // "hello" |
Traps are handler methods that intercept operations on the proxy. JavaScript defines 13 traps covering get/set, property enumeration, function invocation, prototype operations, and more. Each trap corresponds to a Reflect method of the same name.
| Trap | Intercepts | Triggered By |
|---|---|---|
| get | Property access | proxy[key], proxy.key |
| set | Property assignment | proxy[key] = val |
| has | in operator | "key" in proxy |
| deleteProperty | delete operator | delete proxy[key] |
| ownKeys | Object.keys/entries | Object.keys(proxy) |
| getOwnPropertyDescriptor | Property descriptors | Object.getOwnPropertyDescriptor(proxy, key) |
| defineProperty | Object.defineProperty | Object.defineProperty(proxy, key, desc) |
| preventExtensions | Object.preventExtensions | Object.preventExtensions(proxy) |
| isExtensible | Object.isExtensible | Object.isExtensible(proxy) |
| getPrototypeOf | Object.getPrototypeOf | Object.getPrototypeOf(proxy) |
| setPrototypeOf | Object.setPrototypeOf | Object.setPrototypeOf(proxy, proto) |
| apply | Function call | proxy(), proxy.apply() |
| construct | new operator | new proxy() |
| 1 | // Comprehensive trap examples |
| 2 | |
| 3 | const handler = { |
| 4 | // Property access |
| 5 | get(target, prop, receiver) { |
| 6 | console.log(`GET ${String(prop)}`); |
| 7 | return prop in target ? Reflect.get(target, prop, receiver) : "fallback"; |
| 8 | }, |
| 9 | |
| 10 | // Property assignment |
| 11 | set(target, prop, value, receiver) { |
| 12 | console.log(`SET ${String(prop)} = ${value}`); |
| 13 | return Reflect.set(target, prop, value, receiver); |
| 14 | }, |
| 15 | |
| 16 | // in operator |
| 17 | has(target, prop) { |
| 18 | console.log(`HAS ${String(prop)}`); |
| 19 | return Reflect.has(target, prop); |
| 20 | }, |
| 21 | |
| 22 | // delete operator |
| 23 | deleteProperty(target, prop) { |
| 24 | console.log(`DELETE ${String(prop)}`); |
| 25 | return Reflect.deleteProperty(target, prop); |
| 26 | }, |
| 27 | |
| 28 | // Object.keys / for..in |
| 29 | ownKeys(target) { |
| 30 | console.log("OWN_KEYS"); |
| 31 | return Reflect.ownKeys(target); |
| 32 | } |
| 33 | }; |
| 34 | |
| 35 | const obj = { a: 1, b: 2 }; |
| 36 | const proxy = new Proxy(obj, handler); |
| 37 | |
| 38 | proxy.a; // GET a |
| 39 | proxy.c; // GET c → "fallback" |
| 40 | proxy.a = 10; // SET a = 10 |
| 41 | "a" in proxy; // HAS a |
| 42 | delete proxy.a; // DELETE a |
| 43 | Object.keys(proxy); // OWN_KEYS |
The Reflect object provides static methods that correspond to every proxy trap. They perform the default operation for each trap, making them the standard way to forward operations inside handler functions. Using Reflect instead of direct operations ensures consistent behavior with the target object's internal slots.
| 1 | // Reflect methods mirror proxy traps exactly |
| 2 | const target = { x: 1, y: 2 }; |
| 3 | |
| 4 | // Reflect.get — same as target[prop] |
| 5 | console.log(Reflect.get(target, "x")); // 1 |
| 6 | |
| 7 | // Reflect.set — returns boolean success |
| 8 | console.log(Reflect.set(target, "x", 42)); // true |
| 9 | console.log(target.x); // 42 |
| 10 | |
| 11 | // Reflect.has — same as "in" operator |
| 12 | console.log(Reflect.has(target, "y")); // true |
| 13 | |
| 14 | // Reflect.ownKeys — returns all own keys |
| 15 | console.log(Reflect.ownKeys(target)); // ["x", "y"] |
| 16 | |
| 17 | // Reflect.deleteProperty — returns boolean |
| 18 | console.log(Reflect.deleteProperty(target, "y")); // true |
| 19 | |
| 20 | // Reflect.defineProperty |
| 21 | console.log(Reflect.defineProperty(target, "z", { |
| 22 | value: 3, |
| 23 | writable: true |
| 24 | })); // true |
| 25 | |
| 26 | // Essential: Reflect in proxy handlers preserves receiver binding |
| 27 | // Without Reflect: issues with inheritance and getters |
| 28 | const parent = { |
| 29 | get value() { return this._val; } |
| 30 | }; |
| 31 | |
| 32 | const child = new Proxy(parent, { |
| 33 | get(target, prop, receiver) { |
| 34 | // Must use Reflect.get to pass the correct receiver |
| 35 | return Reflect.get(target, prop, receiver); |
| 36 | } |
| 37 | }); |
| 38 | |
| 39 | child._val = "child"; |
| 40 | console.log(child.value); // "child" — correct receiver |
info
Validation
Proxies can enforce type constraints, range limits, and schema validation on object properties without cluttering the business logic with validation code.
| 1 | // Schema validation with Proxy |
| 2 | function createValidatedObject(schema) { |
| 3 | return new Proxy({}, { |
| 4 | set(target, prop, value) { |
| 5 | const rules = schema[prop]; |
| 6 | if (rules) { |
| 7 | if (rules.type && typeof value !== rules.type) { |
| 8 | throw new TypeError(`Expected ${prop} to be ${rules.type}`); |
| 9 | } |
| 10 | if (rules.min !== undefined && value < rules.min) { |
| 11 | throw new RangeError(`${prop} must be >= ${rules.min}`); |
| 12 | } |
| 13 | if (rules.max !== undefined && value > rules.max) { |
| 14 | throw new RangeError(`${prop} must be <= ${rules.max}`); |
| 15 | } |
| 16 | } |
| 17 | return Reflect.set(target, prop, value); |
| 18 | } |
| 19 | }); |
| 20 | } |
| 21 | |
| 22 | const user = createValidatedObject({ |
| 23 | name: { type: "string" }, |
| 24 | age: { type: "number", min: 0, max: 150 } |
| 25 | }); |
| 26 | |
| 27 | user.name = "Alice"; // OK |
| 28 | user.age = 25; // OK |
| 29 | // user.age = -1; // RangeError: age must be >= 0 |
| 30 | // user.age = "old"; // TypeError: Expected age to be number |
Logging & Observability
Proxies can automatically log all property access, mutations, and method calls — invaluable for debugging, auditing, and tracing data flow.
| 1 | // Observable object — logs all operations |
| 2 | function observable(target) { |
| 3 | const handlers = []; |
| 4 | |
| 5 | const proxy = new Proxy(target, { |
| 6 | get(target, prop, receiver) { |
| 7 | if (prop === "onChange") return (fn) => handlers.push(fn); |
| 8 | const value = Reflect.get(target, prop, receiver); |
| 9 | if (typeof value === "function") { |
| 10 | return (...args) => { |
| 11 | const result = value.apply(target, args); |
| 12 | handlers.forEach(h => h(prop, args, result)); |
| 13 | return result; |
| 14 | }; |
| 15 | } |
| 16 | handlers.forEach(h => h("GET", prop, value)); |
| 17 | return value; |
| 18 | }, |
| 19 | set(target, prop, value, receiver) { |
| 20 | const old = target[prop]; |
| 21 | const result = Reflect.set(target, prop, value, receiver); |
| 22 | handlers.forEach(h => h("SET", prop, { from: old, to: value })); |
| 23 | return result; |
| 24 | } |
| 25 | }); |
| 26 | |
| 27 | return proxy; |
| 28 | } |
| 29 | |
| 30 | const data = observable({ count: 0 }); |
| 31 | data.onChange((action, prop, detail) => { |
| 32 | console.log(`[${new Date().toISOString()}]`, action, prop, detail); |
| 33 | }); |
| 34 | |
| 35 | data.count = 1; // [timestamp] SET count { from: 0, to: 1 } |
| 36 | data.count = 2; // [timestamp] SET count { from: 1, to: 2 } |
| 37 | console.log(data.count); // [timestamp] GET count 2 |
Caching & Memoization
Proxy-based caching can transparently memoize computed values and cache results with configurable TTL, all without modifying the original object.
| 1 | // Transparent caching proxy |
| 2 | function withCache(target, ttlMs = 5000) { |
| 3 | const cache = new Map(); |
| 4 | |
| 5 | return new Proxy(target, { |
| 6 | get(target, prop, receiver) { |
| 7 | if (typeof target[prop] !== "function") { |
| 8 | const cached = cache.get(prop); |
| 9 | if (cached && Date.now() < cached.expires) { |
| 10 | console.log(`Cache HIT for ${String(prop)}`); |
| 11 | return cached.value; |
| 12 | } |
| 13 | const value = Reflect.get(target, prop, receiver); |
| 14 | cache.set(prop, { value, expires: Date.now() + ttlMs }); |
| 15 | return value; |
| 16 | } |
| 17 | return (...args) => { |
| 18 | const key = JSON.stringify([prop, args]); |
| 19 | const cached = cache.get(key); |
| 20 | if (cached && Date.now() < cached.expires) { |
| 21 | console.log(`Cache HIT for ${String(prop)}()`); |
| 22 | return cached.value; |
| 23 | } |
| 24 | const result = target[prop](...args); |
| 25 | cache.set(key, { value: result, expires: Date.now() + ttlMs }); |
| 26 | return result; |
| 27 | }; |
| 28 | } |
| 29 | }); |
| 30 | } |
| 31 | |
| 32 | const math = withCache({ |
| 33 | fib: (n) => n <= 1 ? n : math.fib(n - 1) + math.fib(n - 2) |
| 34 | }); |
| 35 | |
| 36 | console.log(math.fib(40)); // Computed (slow) |
| 37 | console.log(math.fib(40)); // Cache HIT — instant |
pro tip
Proxy.revocable creates a proxy that can be revoked (disabled) at any point. Once revoked, any operation on the proxy throws a TypeError. This is useful for providing controlled access to resources that can be revoked after a timeout or when a user logs out.
| 1 | // Revocable proxy — access can be revoked |
| 2 | const resource = { secret: "top-secret-data" }; |
| 3 | |
| 4 | const { proxy, revoke } = Proxy.revocable(resource, { |
| 5 | get(target, prop, receiver) { |
| 6 | if (prop === "secret") { |
| 7 | console.warn("Accessing secret data"); |
| 8 | } |
| 9 | return Reflect.get(target, prop, receiver); |
| 10 | } |
| 11 | }); |
| 12 | |
| 13 | console.log(proxy.secret); // Accessing secret data → "top-secret-data" |
| 14 | |
| 15 | // Revoke access |
| 16 | revoke(); |
| 17 | |
| 18 | // Any subsequent operation throws |
| 19 | try { |
| 20 | console.log(proxy.secret); |
| 21 | } catch (e) { |
| 22 | console.log(e.message); // Cannot perform 'get' on a proxy that has been revoked |
| 23 | } |
| 24 | |
| 25 | // Practical: revoke after timeout |
| 26 | function createTimedAccess(data, ms) { |
| 27 | const { proxy, revoke } = Proxy.revocable(data, {}); |
| 28 | setTimeout(revoke, ms); |
| 29 | return proxy; |
| 30 | } |
| 31 | |
| 32 | const tempAccess = createTimedAccess({ token: "abc" }, 5000); |
| 33 | console.log(tempAccess.token); // "abc" (within 5 seconds) |
| 34 | // After 5 seconds: TypeError |
warning
Proxies introduce overhead because every trapped operation must go through the handler. V8 and other engines cannot apply their normal optimizations (inline caching, hidden classes) to proxy access. For hot-path code — tight loops, frequently accessed properties, performance-critical algorithms — proxies can cause significant degradation.
| 1 | // Performance comparison |
| 2 | const target = { x: 1, y: 2, z: 3 }; |
| 3 | const proxy = new Proxy(target, { |
| 4 | get(t, p, r) { return Reflect.get(t, p, r); } |
| 5 | }); |
| 6 | |
| 7 | const ITERATIONS = 10_000_000; |
| 8 | |
| 9 | // Direct access — optimized by V8 |
| 10 | console.time("direct"); |
| 11 | let sum = 0; |
| 12 | for (let i = 0; i < ITERATIONS; i++) { |
| 13 | sum += target.x + target.y + target.z; |
| 14 | } |
| 15 | console.timeEnd("direct"); |
| 16 | |
| 17 | // Proxy access — significantly slower |
| 18 | console.time("proxy"); |
| 19 | sum = 0; |
| 20 | for (let i = 0; i < ITERATIONS; i++) { |
| 21 | sum += proxy.x + proxy.y + proxy.z; |
| 22 | } |
| 23 | console.timeEnd("proxy"); |
| 24 | |
| 25 | // On V8: direct ~5-10ms, proxy ~200-500ms |
| 26 | // Proxy is 20-100x slower for property access in tight loops |
best practice
pro tip