Skip to main content

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}; call revoke() 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 Reflect methods inside traps to get default behavior with the correct receiver context

Quick example

javascript
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 age

The 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: get and set traps
  • Validate inputs on objects whose shape changes at runtime: set trap
  • Build a read-only view of an object: trap set and deleteProperty to throw
  • Grant temporary, revocable access to sensitive data: Proxy.revocable() with setTimeout on revoke
  • 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

javascript
// 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
javascript
// 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

javascript
// 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]; } });
javascript
// 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

javascript
// 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 ===

javascript
const original = { x: 1 }; const proxy = new Proxy(original, {}); console.log(proxy === original); // false - they are different objects

Proxy creates a separate wrapper. Strict equality always fails. Track the original reference separately if identity comparison matters.

5. Forgetting to revoke a revocable proxy

javascript
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 revoked

Real-world usage

  • Vue 3: dropped Object.defineProperty in 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

javascript
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" = false

Both 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.

javascript
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 string

Validation runs on every assignment to todos. The original state object is never written to directly.

Advanced: revocable access to sensitive data

javascript
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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?