Advanced JavaScript Proxies: Complete Guide

The Proxy object lets you intercept and redefine fundamental operations on any target object. Learn every trap, revocation, and real-world use case for JavaScript proxies.

8 min read

A JavaScript Proxy wraps a target object and sits between it and every piece of code that interacts with it. You define a handler that intercepts specific operations, called traps, and decide what happens when someone reads a property, writes a value, deletes a key, or even calls the object as a function.

javascriptjavascript
const target = { message: "hello" };
 
const handler = {
  get(obj, prop) {
    console.log(`Reading property "${prop}"`);
    return obj[prop];
  }
};
 
const proxy = new Proxy(target, handler);
 
console.log(proxy.message);
// Reading property "message"
// hello

The get trap fires before the value is returned. You can transform it, log it, block it, or return a computed value instead. The original target object never knows it has been wrapped.

How Proxies Intercept Operations

Every interaction with an object ultimately calls an internal method defined in the ECMAScript specification. Proxy handlers override those internal methods:

Proxy interception flow

If the handler defines a trap for the operation, the trap runs and its return value is used. If no trap is defined, the operation falls through to the target object as if the proxy were not there.

The 13 Proxy Traps

The full Proxy API defines traps for every fundamental object operation. Here are all 13, organized by category:

Property Access Traps

TrapInterceptsSignature
getReading a property(target, prop, receiver)
setWriting a property(target, prop, value, receiver)
hasThe in operator(target, prop)
deletePropertyThe delete operator(target, prop)
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor(target, prop)
definePropertyObject.defineProperty(target, prop, descriptor)
ownKeysObject.keys, Object.getOwnPropertyNames, for...in(target)

Function and Construction Traps

TrapInterceptsSignature
applyCalling the proxy as a function(target, thisArg, argumentsList)
constructUsing new on the proxy(target, argumentsList, newTarget)

Prototype and Extensibility Traps

TrapInterceptsSignature
getPrototypeOfObject.getPrototypeOf(target)
setPrototypeOfObject.setPrototypeOf(target, proto)
isExtensibleObject.isExtensible(target)
preventExtensionsObject.preventExtensions(target)

Most real-world proxy use cases only need get, set, has, and deleteProperty, plus occasionally apply or construct. The other traps are for specialized scenarios like virtual object hierarchies or sandboxing.

The Reflect API -- Your Default Companion

Inside every trap, you decide whether to forward the operation to the target or return something else entirely. The Reflect API provides the default behavior for every trap as a simple function call:

javascriptjavascript
const handler = {
  get(target, prop, receiver) {
    if (prop.startsWith("_")) {
      throw new Error(`Cannot access private property "${prop}"`);
    }
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    if (prop.startsWith("_")) {
      throw new Error(`Cannot modify private property "${prop}"`);
    }
    return Reflect.set(target, prop, value, receiver);
  }
};
 
const user = new Proxy({ name: "Alex", _password: "secret" }, handler);
 
console.log(user.name);      // "Alex"
console.log(user._password); // Error: Cannot access private property "_password"

The pattern is always the same: check a condition, then either return your custom logic or call the matching Reflect method. Reflect.get and Reflect.set handle the prototype chain and accessor properties correctly, so you do not need to reimplement those details.

Practical Use Case: Validation Proxy

One of the most common proxy patterns is validating property assignments before they reach the target:

javascriptjavascript
function createValidatedUser(initialData) {
  const target = { ...initialData };
 
  const handler = {
    set(obj, prop, value) {
      if (prop === "age") {
        if (typeof value !== "number" || value < 0 || value > 150) {
          throw new TypeError(`Invalid age: ${value}`);
        }
      }
      if (prop === "email") {
        if (!value.includes("@")) {
          throw new TypeError(`Invalid email: ${value}`);
        }
      }
      obj[prop] = value;
      return true;
    }
  };
 
  return new Proxy(target, handler);
}

The set trap checks the property name before writing. An invalid age or email throws a TypeError instead of silently storing bad data:

javascriptjavascript
const user = createValidatedUser({ name: "Sam", age: 28, email: "sam@example.com" });
 
user.age = 30;             // OK
user.age = -5;             // TypeError: Invalid age: -5
user.email = "not-valid";  // TypeError: Invalid email: not-valid

Every assignment passes through the set trap, which validates before writing. The target object stays clean because no incomplete or invalid data can reach it. This pattern is especially useful for form state, API request bodies, and configuration objects.

Practical Use Case: Logging and Debugging

A proxy can log every interaction with an object without changing any of the code that uses it:

javascriptjavascript
function createLoggingProxy(obj, label = "Object") {
  return new Proxy(obj, {
    get(target, prop) {
      const value = Reflect.get(target, prop);
      console.log(`${label}: GET "${String(prop)}" →`, value);
      return value;
    },
    set(target, prop, value) {
      console.log(`${label}: SET "${String(prop)}" =`, value);
      return Reflect.set(target, prop, value);
    },
    deleteProperty(target, prop) {
      console.log(`${label}: DELETE "${String(prop)}"`);
      return Reflect.deleteProperty(target, prop);
    }
  });
}
 
const config = createLoggingProxy({ debug: false, theme: "dark" }, "Config");
 
config.theme = "light";     // Config: SET "theme" = light
console.log(config.debug);  // Config: GET "debug" → false
delete config.debug;        // Config: DELETE "debug"

Add this wrapper during development to trace which code is reading or writing which properties, then remove it in production with zero changes to the rest of your application.

Revocable Proxies

A revocable proxy can be cut off from its target permanently. Once revoked, any operation on the proxy throws a TypeError:

javascriptjavascript
const { proxy, revoke } = Proxy.revocable(
  { data: "sensitive" },
  {}
);
 
console.log(proxy.data); // "sensitive"
 
revoke();
 
console.log(proxy.data); // TypeError: Cannot perform 'get' on a proxy
                         // that has been revoked

Revocable proxies are ideal for resource lifecycle management. Give a consumer access to an object, and when they are done, revoke the proxy. Any code that holds a reference to the proxy can no longer reach the target.

This pattern appears in security-sensitive APIs where you want to hand out temporary access and guarantee it cannot be used after a certain point. There is no way to un-revoke a proxy.

Practical Use Case: Default Values

A proxy can supply default values for missing properties instead of returning undefined:

javascriptjavascript
function withDefaults(obj, defaultValue) {
  return new Proxy(obj, {
    get(target, prop) {
      return prop in target ? target[prop] : defaultValue;
    }
  });
}
 
const settings = withDefaults({ theme: "dark" }, "not set");
 
console.log(settings.theme);    // "dark"
console.log(settings.language); // "not set"

The in operator checks whether the property exists on the target, including its prototype chain. If it does not, the proxy returns the fallback value instead of undefined. This is simpler and more readable than sprinkling a default value check throughout your code.

Practical Use Case: Negative Array Indices

A proxy can make arrays support Python-style negative indexing, where an index of -1 means the last element:

javascriptjavascript
function createArray(...items) {
  return new Proxy(items, {
    get(target, prop) {
      const index = Number(prop);
      if (!isNaN(index) && index < 0) {
        return target[target.length + index];
      }
      return Reflect.get(target, prop);
    }
  });
}
 
const arr = createArray("a", "b", "c", "d");
 
console.log(arr[-1]); // "d"
console.log(arr[-2]); // "c"
console.log(arr[0]);  // "a"

The trap converts negative numeric indices by adding them to the array length before forwarding to the target. Non-numeric properties and positive indices pass through via Reflect.get.

Combining Traps for a Rich Wrapper

Real proxy patterns often combine multiple traps. Here is a proxy that provides both default values and read-only protection for specific properties:

javascriptjavascript
function createReadOnlyWithDefaults(obj, readOnlyKeys, defaultValue) {
  return new Proxy(obj, {
    get(target, prop) {
      return prop in target ? target[prop] : defaultValue;
    },
    set(target, prop, value) {
      if (readOnlyKeys.includes(prop)) {
        throw new Error(`Cannot modify read-only property "${String(prop)}"`);
      }
      return Reflect.set(target, prop, value);
    },
    deleteProperty(target, prop) {
      if (readOnlyKeys.includes(prop)) {
        throw new Error(`Cannot delete read-only property "${String(prop)}"`);
      }
      return Reflect.deleteProperty(target, prop);
    }
  });
}

The get trap fills in missing properties, while set and deleteProperty block writes to the keys listed in readOnlyKeys. Calling the function below wires all three traps onto the same target object:

javascriptjavascript
const user = createReadOnlyWithDefaults(
  { id: 1, name: "Alex" },
  ["id"],
  "N/A"
);
 
console.log(user.id);      // 1
console.log(user.email);   // "N/A" (default)
user.name = "Bob";         // OK
user.id = 2;               // Error: Cannot modify read-only property "id"

This pattern uses three traps working together to create a consistent interface. The target object remains a plain object, so any code can still access it directly if needed. The proxy is an additional access layer, not a replacement for the target.

Proxy Limitations

Proxies are powerful but not transparent in all situations:

  • Equality: proxy === target is always false. The proxy is a distinct object.
  • Built-in internal slots: Proxying Map, Set, Date, and Promise instances requires extra care because internal methods may not forward through a plain target. Bind methods explicitly to the target when wrapping built-ins.
  • No trap for strict equality or typeof: You cannot intercept the equality or typeof operators.
  • Performance: Proxies add measurable overhead. Do not wrap objects accessed in tight loops or rendering hot paths.

For the proxy design pattern (virtual proxy, caching proxy, lazy initialization), see /javascript/the-javascript-proxy-pattern-complete-guide. For reactive state powered by Proxy, see /javascript/building-a-reactive-ui-with-the-js-observer.

Rune AI

Rune AI

Key Insights

  • Proxy wraps a target object and intercepts operations through handler traps.
  • There are 13 traps covering get, set, delete, function calls, construction, and more.
  • Always use Reflect inside traps to forward default behavior where needed.
  • Revocable proxies let you cut off access to an object entirely after a certain point.
  • Proxy powers Vue 3 reactivity, ORM lazy loading, and API mocking libraries.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between Proxy and Object.defineProperty?

Object.defineProperty can only intercept property access and assignment on known property names. Proxy intercepts every operation (property access, deletion, function calls, construction, prototype checks) on any property, including ones that do not exist yet.

Does Proxy affect performance?

Yes. Proxy adds an indirection layer for every intercepted operation. For performance-critical hot paths, avoid wrapping objects that are accessed thousands of times per frame. For most application code (form validation, API wrappers, access control), the overhead is negligible.

Conclusion

The Proxy API is JavaScript's most powerful metaprogramming tool. It lets you stand between any object and the code that uses it, intercepting every fundamental operation. Whether you are building validation layers, reactive state systems, API wrappers, or access control, Proxy gives you a structured, safe way to modify behavior without rewriting source code.