Advanced JavaScript Proxies Complete Guide

Master JavaScript Proxy objects for meta-programming. Covers all 13 proxy traps, handler invariants, revocable proxies, proxy chains, validation layers, virtual object patterns, lazy initialization, access control, and performance considerations for production use.

JavaScriptadvanced
18 min read

JavaScript's Proxy object intercepts and customizes fundamental operations on objects. It enables powerful meta-programming patterns including validation, access control, lazy initialization, and virtual properties through 13 handler traps that correspond to internal object operations.

For how Proxy works alongside the Reflect API, see Using Reflect and Proxy Together in JavaScript.

The 13 Proxy Traps

javascriptjavascript
// Every operation on an object has a corresponding proxy trap
// Here is the complete handler with all 13 traps:
 
const completeHandler = {
  // 1. Property read: obj.prop, obj['prop']
  get(target, property, receiver) {
    console.log(`get: ${String(property)}`);
    return Reflect.get(target, property, receiver);
  },
 
  // 2. Property write: obj.prop = value
  set(target, property, value, receiver) {
    console.log(`set: ${String(property)} = ${value}`);
    return Reflect.set(target, property, value, receiver);
  },
 
  // 3. Property existence: 'prop' in obj
  has(target, property) {
    console.log(`has: ${String(property)}`);
    return Reflect.has(target, property);
  },
 
  // 4. Property deletion: delete obj.prop
  deleteProperty(target, property) {
    console.log(`delete: ${String(property)}`);
    return Reflect.deleteProperty(target, property);
  },
 
  // 5. Own property keys: Object.keys, Object.getOwnPropertyNames, for..in
  ownKeys(target) {
    console.log("ownKeys");
    return Reflect.ownKeys(target);
  },
 
  // 6. Property descriptor: Object.getOwnPropertyDescriptor(obj, prop)
  getOwnPropertyDescriptor(target, property) {
    console.log(`getOwnPropertyDescriptor: ${String(property)}`);
    return Reflect.getOwnPropertyDescriptor(target, property);
  },
 
  // 7. Define property: Object.defineProperty(obj, prop, descriptor)
  defineProperty(target, property, descriptor) {
    console.log(`defineProperty: ${String(property)}`);
    return Reflect.defineProperty(target, property, descriptor);
  },
 
  // 8. Prototype read: Object.getPrototypeOf(obj)
  getPrototypeOf(target) {
    console.log("getPrototypeOf");
    return Reflect.getPrototypeOf(target);
  },
 
  // 9. Prototype write: Object.setPrototypeOf(obj, proto)
  setPrototypeOf(target, prototype) {
    console.log("setPrototypeOf");
    return Reflect.setPrototypeOf(target, prototype);
  },
 
  // 10. Extensibility check: Object.isExtensible(obj)
  isExtensible(target) {
    console.log("isExtensible");
    return Reflect.isExtensible(target);
  },
 
  // 11. Prevent extensions: Object.preventExtensions(obj)
  preventExtensions(target) {
    console.log("preventExtensions");
    return Reflect.preventExtensions(target);
  },
 
  // 12. Function call: proxy(args)
  apply(target, thisArg, argumentsList) {
    console.log(`apply with ${argumentsList.length} args`);
    return Reflect.apply(target, thisArg, argumentsList);
  },
 
  // 13. Constructor call: new proxy(args)
  construct(target, argumentsList, newTarget) {
    console.log(`construct with ${argumentsList.length} args`);
    return Reflect.construct(target, argumentsList, newTarget);
  }
};
 
// Usage: wrap any object with all 13 traps
const target = { x: 1, y: 2 };
const proxy = new Proxy(target, completeHandler);
 
proxy.x;           // get: x
proxy.z = 3;       // set: z = 3
"x" in proxy;      // has: x
delete proxy.z;    // delete: z
Object.keys(proxy); // ownKeys

Validation and Type Enforcement

javascriptjavascript
// Proxy enables runtime type checking on object properties
 
function createTypedObject(schema) {
  const data = {};
 
  return new Proxy(data, {
    set(target, property, value) {
      const validator = schema[property];
      if (!validator) {
        throw new TypeError(`Unknown property: ${property}`);
      }
 
      const result = validator(value);
      if (result !== true) {
        throw new TypeError(
          `Invalid value for ${property}: ${result || "validation failed"}`
        );
      }
 
      target[property] = value;
      return true;
    },
 
    get(target, property) {
      if (property in target) return target[property];
      if (property in schema) return undefined; // Known but unset
      throw new TypeError(`Unknown property: ${property}`);
    }
  });
}
 
// Schema definition with validators
const userSchema = {
  name: (v) => typeof v === "string" && v.length > 0 || "must be a non-empty string",
  age: (v) => Number.isInteger(v) && v >= 0 && v <= 150 || "must be an integer 0-150",
  email: (v) => typeof v === "string" && v.includes("@") || "must contain @",
  role: (v) => ["admin", "user", "editor"].includes(v) || "must be admin, user, or editor"
};
 
const user = createTypedObject(userSchema);
user.name = "Alice";      // OK
user.age = 30;            // OK
user.email = "a@b.com";   // OK
// user.age = -5;          // TypeError: must be an integer 0-150
// user.unknown = "x";     // TypeError: Unknown property: unknown
 
// IMMUTABLE OBJECT PATTERN
function createImmutable(obj) {
  return new Proxy(obj, {
    set() {
      throw new TypeError("Cannot modify an immutable object");
    },
    deleteProperty() {
      throw new TypeError("Cannot delete from an immutable object");
    },
    defineProperty() {
      throw new TypeError("Cannot define properties on an immutable object");
    },
    setPrototypeOf() {
      throw new TypeError("Cannot change prototype of an immutable object");
    },
    get(target, property, receiver) {
      const value = Reflect.get(target, property, receiver);
      // Deep immutability: wrap nested objects too
      if (typeof value === "object" && value !== null) {
        return createImmutable(value);
      }
      return value;
    }
  });
}
 
const config = createImmutable({
  db: { host: "localhost", port: 5432 },
  cache: { ttl: 3600 }
});
 
console.log(config.db.host);    // "localhost"
// config.db.host = "remote";   // TypeError: Cannot modify an immutable object

Virtual Objects and Lazy Initialization

javascriptjavascript
// VIRTUAL PROPERTIES: Properties that don't exist on the target
// but are computed on access
 
function createComputedObject(data, computedProps) {
  return new Proxy(data, {
    get(target, property, receiver) {
      if (property in computedProps) {
        return computedProps[property](target);
      }
      return Reflect.get(target, property, receiver);
    },
 
    has(target, property) {
      return property in computedProps || Reflect.has(target, property);
    },
 
    ownKeys(target) {
      return [...Reflect.ownKeys(target), ...Object.keys(computedProps)];
    },
 
    getOwnPropertyDescriptor(target, property) {
      if (property in computedProps) {
        return {
          configurable: true,
          enumerable: true,
          value: computedProps[property](target),
          writable: false
        };
      }
      return Reflect.getOwnPropertyDescriptor(target, property);
    }
  });
}
 
const rectangle = createComputedObject(
  { width: 10, height: 5 },
  {
    area: (t) => t.width * t.height,
    perimeter: (t) => 2 * (t.width + t.height),
    diagonal: (t) => Math.sqrt(t.width ** 2 + t.height ** 2)
  }
);
 
console.log(rectangle.area);      // 50
console.log(rectangle.perimeter); // 30
console.log(rectangle.diagonal);  // 11.18...
rectangle.width = 20;
console.log(rectangle.area);      // 100 (recomputed)
 
// LAZY INITIALIZATION PATTERN
function createLazy(factory) {
  let instance = null;
 
  return new Proxy({}, {
    get(target, property, receiver) {
      if (!instance) {
        instance = factory();
        console.log("Lazy instance created");
      }
      return Reflect.get(instance, property, receiver);
    },
 
    set(target, property, value, receiver) {
      if (!instance) {
        instance = factory();
        console.log("Lazy instance created");
      }
      return Reflect.set(instance, property, value, receiver);
    }
  });
}
 
const expensiveService = createLazy(() => {
  // Only created when first accessed
  return {
    data: loadExpensiveData(),
    process(input) { return input; }
  };
});
 
function loadExpensiveData() {
  return { items: [1, 2, 3] };
}
 
// No instance created yet
console.log("Service defined but not initialized");
// First access triggers creation
console.log(expensiveService.data); // "Lazy instance created" then { items: [1,2,3] }

Revocable Proxies and Access Control

javascriptjavascript
// REVOCABLE PROXIES: Can be "turned off" to prevent further access
 
function createSecureSession(data, expiresInMs) {
  const { proxy, revoke } = Proxy.revocable(data, {
    get(target, property) {
      console.log(`[Session] Reading: ${String(property)}`);
      return Reflect.get(target, property);
    },
 
    set(target, property, value) {
      console.log(`[Session] Writing: ${String(property)}`);
      return Reflect.set(target, property, value);
    }
  });
 
  // Auto-revoke after expiration
  setTimeout(() => {
    revoke();
    console.log("[Session] Expired and revoked");
  }, expiresInMs);
 
  return { proxy, revoke };
}
 
const session = createSecureSession(
  { userId: 123, role: "admin" },
  5000 // 5 second session
);
 
session.proxy.userId; // OK: 123
// After 5 seconds: session.proxy.userId throws TypeError
 
// ROLE-BASED ACCESS CONTROL
function createAccessControlled(data, permissions) {
  return new Proxy(data, {
    get(target, property, receiver) {
      const perm = permissions[property];
      if (perm && !perm.read) {
        throw new Error(`Access denied: cannot read '${String(property)}'`);
      }
      return Reflect.get(target, property, receiver);
    },
 
    set(target, property, value, receiver) {
      const perm = permissions[property];
      if (perm && !perm.write) {
        throw new Error(`Access denied: cannot write '${String(property)}'`);
      }
      return Reflect.set(target, property, value, receiver);
    },
 
    deleteProperty(target, property) {
      const perm = permissions[property];
      if (perm && !perm.delete) {
        throw new Error(`Access denied: cannot delete '${String(property)}'`);
      }
      return Reflect.deleteProperty(target, property);
    },
 
    ownKeys(target) {
      // Only expose readable properties
      return Reflect.ownKeys(target).filter((key) => {
        const perm = permissions[key];
        return !perm || perm.read !== false;
      });
    }
  });
}
 
const sensitiveData = createAccessControlled(
  { name: "Alice", ssn: "123-45-6789", email: "alice@example.com" },
  {
    ssn: { read: false, write: false, delete: false },
    name: { read: true, write: true, delete: false },
    email: { read: true, write: true, delete: false }
  }
);
 
console.log(sensitiveData.name);    // "Alice"
// sensitiveData.ssn;               // Error: Access denied
console.log(Object.keys(sensitiveData)); // ["name", "email"] (ssn hidden)

Proxy Chains and Composition

javascriptjavascript
// Stack multiple proxies for layered behavior
 
function withLogging(target) {
  return new Proxy(target, {
    get(target, property, receiver) {
      const value = Reflect.get(target, property, receiver);
      console.log(`[LOG] get ${String(property)} -> ${JSON.stringify(value)}`);
      return value;
    },
    set(target, property, value, receiver) {
      console.log(`[LOG] set ${String(property)} = ${JSON.stringify(value)}`);
      return Reflect.set(target, property, value, receiver);
    }
  });
}
 
function withCache(target, ttlMs = 5000) {
  const cache = new Map();
 
  return new Proxy(target, {
    get(target, property, receiver) {
      const cached = cache.get(property);
      if (cached && Date.now() - cached.time < ttlMs) {
        return cached.value;
      }
      const value = Reflect.get(target, property, receiver);
      cache.set(property, { value, time: Date.now() });
      return value;
    },
    set(target, property, value, receiver) {
      cache.delete(property);
      return Reflect.set(target, property, value, receiver);
    }
  });
}
 
function withDefaults(target, defaults) {
  return new Proxy(target, {
    get(target, property, receiver) {
      const value = Reflect.get(target, property, receiver);
      if (value === undefined && property in defaults) {
        return defaults[property];
      }
      return value;
    }
  });
}
 
// Compose proxies: logging -> caching -> defaults -> target
const data = { name: "Alice" };
const withDef = withDefaults(data, { role: "user", theme: "dark" });
const cached = withCache(withDef, 3000);
const logged = withLogging(cached);
 
console.log(logged.name);   // [LOG] get name -> "Alice"
console.log(logged.role);   // [LOG] get role -> "user" (default)
console.log(logged.theme);  // [LOG] get theme -> "dark" (default)
TrapTriggered ByMust Return
getProperty read, Reflect.getAny value
setProperty write, Reflect.setBoolean (true = success)
hasin operator, Reflect.hasBoolean
deletePropertydelete operatorBoolean
ownKeysObject.keys, for...inArray of strings/symbols
applyFunction call proxy(args)Any value
constructnew proxy(args)Object
Rune AI

Rune AI

Key Insights

  • Proxy provides 13 traps that map to every internal object operation from property access to prototype manipulation: Each trap receives the target object and operation parameters, enabling complete interception
  • Handler invariants prevent proxies from violating non-configurable property constraints, enforced by the engine: The JavaScript specification requires traps to be consistent with the target's actual property descriptors
  • Revocable proxies can be permanently disabled to cut off access and enable garbage collection of the target: Use Proxy.revocable() for session management, temporary grants, and security boundaries
  • Proxy composition through chaining enables layered behaviors like logging, caching, and defaults on the same object: Each proxy layer handles one concern, following the single responsibility principle
  • Proxy access is 5-50x slower than direct property access because V8 cannot apply inline caching or hidden class optimizations: Use proxies at boundaries, not in hot loops
RunePowered by Rune AI

Frequently Asked Questions

Do Proxy traps affect performance?

Yes. Each intercepted operation goes through the trap function instead of the engine's optimized internal path. Property access on a Proxy is typically 5-50x slower than direct access. V8 cannot optimize proxy property access the way it optimizes regular objects (no hidden classes, no inline caching). For hot loops, avoid proxied objects. Use proxies at API boundaries, configuration layers, or development-time validation, and unwrap to plain objects for performance-critical paths.

What are handler invariants?

Proxy traps must follow certain rules (invariants) enforced by the JavaScript engine. For example, the `get` trap cannot return a different value for a non-writable, non-configurable property than what the target holds. The `has` trap cannot report a non-configurable property as non-existent. The `ownKeys` trap must include all non-configurable own properties. Violating these invariants throws a TypeError. They exist to prevent proxies from lying about fundamental object guarantees.

Can I proxy built-in objects like Array or Map?

Partially. Arrays work with Proxy but some internal methods bypass traps. Map and Set use internal slots that proxies cannot intercept, so wrapping a Map in a Proxy breaks `.get()` and `.set()` methods because `this` inside those methods refers to the proxy rather than the original Map. The workaround is to bind all methods to the original target in the `get` trap, returning `target[property].bind(target)` for function properties.

How do revocable proxies help with memory management?

Revocable proxies created with `Proxy.revocable()` can be permanently disabled by calling the `revoke()` function. After revocation, any operation on the proxy throws a TypeError and the internal reference to the target object is cleared. This allows the target to be garbage collected even if the proxy reference survives. This pattern is useful for session objects, temporary access grants, and plugin sandboxing where you need to guarantee that access can be cut off.

Conclusion

JavaScript Proxy provides 13 traps that intercept every fundamental object operation. Use proxies for validation, access control, lazy initialization, and virtual properties at API boundaries. Combine proxies through composition for layered behavior. For integrating Proxy with the Reflect API, see Using Reflect and Proxy Together in JavaScript. For data binding patterns built on proxies, explore Data Binding with JS Proxies Complete Guide.