What is Proxy Object in JavaScript
Proxy object is a built-in JavaScript constructor that wraps a target object and intercepts its fundamental operations through a handler containing optional trap methods.
Theory
TL;DR
- Think of it as a security checkpoint: every read, write, or delete on the proxy passes through your trap function before (or instead of) reaching the target
- Main split from
Object.defineProperty: Proxy traps ALL operations on ANY property, including ones that don't exist yet; defineProperty only covers statically known keys Proxy.revocable()returns{proxy, revoke}; callrevoke()and every subsequent operation on that proxy throws permanently- V8 benchmarks put Proxy at 2-5x slower than direct property access; skip it for tight loops
- Always use
Reflectmethods inside traps to get default behavior with the correctreceivercontext
Quick example
const user = { name: 'Alice', age: 25 };
const proxy = new Proxy(user, {
get(target, prop, receiver) {
console.log(`Accessing ${prop}`);
return Reflect.get(target, prop, receiver); // preserve receiver for correct `this`
}
});
console.log(proxy.name); // Accessing name -> Alice
proxy.age; // Accessing ageThe third argument receiver matters when methods use this. Without it, calls through an inherited prototype chain break in ways that are hard to debug.
Proxy vs Object.defineProperty
Object.defineProperty patches one known property at definition time. It cannot intercept the in operator, for...in, delete, or any property added after the fact. Proxy operates at the object level: one handler covers every operation on the target, including dynamic keys and future additions.
That gap is exactly why Vue 3 replaced the Object.defineProperty-based reactivity from Vue 2 with a Proxy-based system. The old approach missed array index assignments and new property additions. Proxy catches both.
When to use
- Log or audit every property read/write during development:
getandsettraps - Validate inputs on objects whose shape changes at runtime:
settrap - Build a read-only view of an object: trap
setanddeletePropertyto throw - Grant temporary, revocable access to sensitive data:
Proxy.revocable()withsetTimeoutonrevoke - Implement reactive state like Vue 3 observables or MobX
One observation from production: a validation Proxy that ended up inside a React render cycle caused visible frame drops. The profiler caught it immediately, but it was a clear reminder that Proxy overhead compounds fast when call count scales.
How the JavaScript engine handles Proxy
V8 stores a hidden handler slot on every Proxy instance. When you read proxy.prop, V8 checks whether the handler has a get trap. If yes, it calls your function with (target, prop, receiver). If the trap is absent, V8 falls through to Reflect.get(target, prop, receiver) by default. The same logic applies to set, deleteProperty, has, ownKeys, apply, construct, and the other fundamental operations defined in the spec.
Revocable proxies store a separate revoke function that nulls the handler slot when called. After revocation, any operation on the proxy throws a TypeError with no way to restore it.
Common mistakes
1. Using the proxy reference inside its own trap
// Wrong: p is the proxy, so p[prop] triggers get again
const p = new Proxy({}, {
get(target, prop) {
return p[prop]; // infinite recursion
}
});
p.x; // RangeError: Maximum call stack size exceeded// Fix: use target (the raw object), not the proxy variable
const p = new Proxy({}, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
}
});2. Skipping the receiver argument breaks method calls
// Wrong: no receiver means `this` won't point to the proxy in inherited methods
const p = new Proxy(obj, {
get(target, prop) {
return target[prop];
}
});// Fix: pass receiver so `this` stays correct through prototype chains
const p = new Proxy(obj, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
}
});3. Mutating target directly inside a trap bypasses the set trap
// Wrong: direct mutation on target skips set trap logic entirely
const p = new Proxy({ count: 0 }, {
get(target, prop) {
target.count++; // no set trap fires here
return target[prop];
}
});If you need to write a value from inside a trap, use Reflect.set(target, prop, value, receiver) so the full trap chain runs.
4. Comparing proxy to original with ===
const original = { x: 1 };
const proxy = new Proxy(original, {});
console.log(proxy === original); // false - they are different objectsProxy creates a separate wrapper. Strict equality always fails. Track the original reference separately if identity comparison matters.
5. Forgetting to revoke a revocable proxy
const secret = { apiKey: 'xyz-123' };
const { proxy, revoke } = Proxy.revocable(secret, {});
// Without revoke(), the proxy keeps secret reachable indefinitely
setTimeout(revoke, 1000);
console.log(proxy.apiKey); // xyz-123 (before revoke)
// After revoke: TypeError: Cannot perform 'get' on a proxy that has been revokedReal-world usage
- Vue 3: dropped
Object.definePropertyin favor of Proxy to handle array mutations and dynamic keys that Vue 2 missed - MobX: observable objects use Proxy traps to track reads and writes and trigger reactions
- Immer: wraps a draft state object in a Proxy so you write mutations that produce immutable updates
- Zustand: uses Proxy internally for devtools inspection of state changes
- Node.js vm2: sandboxed code runs inside revocable Proxies that restrict access to host objects
Follow-up questions
Q: What is the full signature of the get trap?
A: get(target, property, receiver). target is the wrapped object, property is the key being accessed, receiver is the proxy itself (or the object that initiated the lookup through a prototype chain).
Q: How does Proxy intercept for...in loops?
A: Via the ownKeys trap. Return an empty array from it and for...in sees no properties. Without a trap, the loop behaves normally against the target.
Q: What exactly happens after revoke() is called?
A: The handler slot is set to null. Every subsequent operation on the proxy throws a TypeError immediately. There is no way to re-enable it.
Q: Why is Proxy slower than direct property access?
A: Each intercepted operation goes through an extra function call and internal handler lookup in V8. That overhead is roughly 2-5x compared to reading a plain property. It matters in hot code paths, not in typical application logic.
Q: How would you implement a deep validation Proxy? (senior level)
A: In the set trap, check whether the incoming value is an object or array. If it is, wrap it in another Proxy before assigning and recurse. That way every write at any depth goes through validation, not just the top-level assignment.
Examples
Basic: logging property access
const config = { debug: true, timeout: 3000 };
const trackedConfig = new Proxy(config, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
console.log(`[config] read "${prop}" = ${value}`);
return value;
},
set(target, prop, value, receiver) {
console.log(`[config] set "${prop}" = ${value}`);
return Reflect.set(target, prop, value, receiver);
}
});
trackedConfig.timeout; // [config] read "timeout" = 3000
trackedConfig.debug = false; // [config] set "debug" = falseBoth traps delegate to Reflect with the full signature. The target stays untouched; all side effects live in the handler.
Intermediate: runtime validation on a state object
This follows the same pattern Zustand uses in its devtools middleware.
const state = {
todos: [{ id: 1, text: 'Buy milk', done: false }]
};
const validatedState = new Proxy(state, {
set(target, prop, value, receiver) {
if (prop === 'todos') {
if (!Array.isArray(value)) throw new TypeError('todos must be an array');
value.forEach(todo => {
if (typeof todo.text !== 'string') throw new TypeError('todo.text must be a string');
});
}
return Reflect.set(target, prop, value, receiver);
}
});
validatedState.todos = [{ id: 2, text: 'Walk dog', done: false }]; // OK
validatedState.todos = [{ id: 3, text: 123, done: false }]; // TypeError: todo.text must be a stringValidation runs on every assignment to todos. The original state object is never written to directly.
Advanced: revocable access to sensitive data
function createTemporaryAccess(data, durationMs) {
const { proxy, revoke } = Proxy.revocable(data, {
get(target, prop, receiver) {
console.log(`[access] reading ${String(prop)}`);
return Reflect.get(target, prop, receiver);
}
});
setTimeout(revoke, durationMs);
return proxy;
}
const credentials = createTemporaryAccess({ apiKey: 'xyz-123' }, 2000);
console.log(credentials.apiKey); // [access] reading apiKey -> xyz-123
setTimeout(() => {
try {
console.log(credentials.apiKey);
} catch (e) {
console.log(e.message); // Cannot perform 'get' on a proxy that has been revoked
}
}, 3000);After revoke() runs, the credentials reference still exists in memory, but every operation on it throws. The underlying data object becomes eligible for garbage collection if nothing else holds a reference to it.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.