← thecodex.expert · The Codex Family of Knowledge
The Codex / JavaScript / Proxy & Reflect

Proxy & Reflect

Intercept and customise fundamental object operations — get, set, delete, function calls, and more — with Proxy traps and the Reflect API.

What it is, in plain English: A Proxy wraps an object and lets you intercept (trap) fundamental operations on it: reading a property, writing a property, deleting a property, calling a function, and more. For every trap you define, the corresponding operation is redirected through your handler. The Reflect API provides the default behaviour for each operation — so you can intercept, do something extra, and then delegate the real work to Reflect.
Where this bites you

A Proxy only guards the object you wrap, not the objects inside it. Reading config.db through a read-only proxy hands back the plain inner object, and writes to that object go straight through with no error. The same applies to validation and change-tracking proxies: nested changes skip your traps entirely. Object.freeze is shallow in exactly the same way.

function readonly(target) {
  return new Proxy(target, {
    set(_, key) {
      throw new TypeError(`Cannot set "${key}" — object is read-only`);
    },
    deleteProperty(_, key) {
      throw new TypeError(`Cannot delete "${key}" — object is read-only`);
    }
  });
}

const config = readonly({ endpoint: "/api", db: { password: "s3cret" } });
config.db.password = "hacked";     // no error: config.db is the plain inner object
console.log(config.db.password);
// hacked

Wrapping objects to intercept their operations

A Proxy sits in front of an object. Every time code tries to read, write, or delete a property on the proxy, your handler function gets called first — giving you a chance to validate, log, transform, or block the operation.

Creating a Proxy

// new Proxy(target, handler)
// target  — the real object being wrapped
// handler — an object with "trap" methods

const person = { name: "Alice", age: 30 };

const proxy = new Proxy(person, {
  get(target, key) {
    // Called every time proxy.someProperty is read
    console.log(`→ Reading "${key}"`);
    return target[key];  // return the actual value
  },
  set(target, key, value) {
    // Called every time proxy.someProperty = value
    console.log(`→ Writing "${key}" = ${value}`);
    target[key] = value;
    return true;   // must return true to indicate success
  }
});

proxy.name;         // logs: → Reading "name"
proxy.age = 31;     // logs: → Writing "age" = 31

The most useful trap: validation

function createValidatedUser(data) {
  return new Proxy(data, {
    set(target, key, value) {
      if (key === "age") {
        if (typeof value !== "number") throw new TypeError("age must be a number");
        if (value < 0 || value > 150)  throw new RangeError("age must be 0–150");
      }
      if (key === "email" && !value.includes("@")) {
        throw new Error("Invalid email address");
      }
      target[key] = value;
      return true;
    }
  });
}

const user = createValidatedUser({ name: "Bob" });
user.name  = "Bob";         // fine
user.age   = 25;            // fine
// user.age   = -5;         // throws: age must be 0–150
// user.email = "notvalid"; // throws: Invalid email address

Reflect — the default implementation

Inside a trap, use Reflect to perform the operation normally. This is cleaner than manually doing target[key] because Reflect handles edge cases correctly (inherited properties, getters/setters, etc.).

const handler = {
  get(target, key, receiver) {
    console.log(`Reading "${key}"`);
    return Reflect.get(target, key, receiver);  // same as: target[key]
  },
  set(target, key, value, receiver) {
    console.log(`Writing "${key}"`);
    return Reflect.set(target, key, value, receiver);  // same as: target[key] = value
  }
};

const proxy = new Proxy({ x: 1 }, handler);
proxy.x;      // "Reading x"
proxy.y = 2;  // "Writing y"
Try It Yourself

Official Sources

The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.