JavaScriptProxy & Reflect

Proxy and Reflect

A Proxy lets you wrap an object and intercept the fundamental operations performed on it — reading a property, writing one, deleting one, checking with in, even calling it as a function. Reflect is its companion: a set of methods that perform those same operations on a plain object, so the inside of a proxy handler can do "the default thing" without trying to recreate it by hand.

The shape of a Proxy

JS
const target = { name: "Ada", age: 36 };

const handler = {
  get(obj, prop) {
    console.log("read", prop);
    return obj[prop];
  },
  set(obj, prop, value) {
    console.log("write", prop, "=", value);
    obj[prop] = value;
    return true;
  },
};

const p = new Proxy(target, handler);

p.name;            // logs: read name -> "Ada"
p.age = 37;        // logs: write age = 37
p.name;            // logs: read name -> "Ada"
console.log(target.age);   // 37

A handler is just an object of optional trap methods. Any trap you do not provide falls through to the default behaviour on the target.

The common traps
  • get(target, prop, receiver) — reading a property.

  • set(target, prop, value, receiver) — assigning a property. Must return true for success.

  • has(target, prop) — the in operator.

  • deleteProperty(target, prop) — the delete operator.

  • ownKeys(target) — what Object.keys, for..in and spread see.

  • getOwnPropertyDescriptor, defineProperty — fine-grained property control.

  • apply(target, thisArg, args) — calling the proxy as a function.

  • construct(target, args) — calling it with new.

Reflect — the matched API

Reflect mirrors every trap with a function that performs the default operation. Inside a handler, calling Reflect.get(target, prop, receiver) does exactly what would have happened without your proxy — useful for "intercept and forward" patterns.

JS
const logged = new Proxy({ x: 1 }, {
  get(target, prop, receiver) {
    console.log("get", prop);
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    console.log("set", prop, "=", value);
    return Reflect.set(target, prop, value, receiver);
  },
});

logged.x;          // get x -> 1
logged.x = 2;      // set x = 2

The handler logs, then forwards. Without Reflect, you would manually assign target[prop] = value and remember to return true — Reflect does both.

Use case 1 — validation

Block bad writes at the boundary.

JS
function validatedUser(initial) {
  return new Proxy(initial, {
    set(target, prop, value) {
      if (prop === "age" && (typeof value !== "number" || value < 0)) {
        throw new TypeError("age must be a non-negative number");
      }
      if (prop === "email" && !String(value).includes("@")) {
        throw new TypeError("email must contain @");
      }
      return Reflect.set(target, prop, value);
    },
  });
}

const user = validatedUser({ name: "Ada", age: 36 });
user.age = 37;             // ok
// user.age = -1;          // TypeError
Use case 2 — observables

Notify subscribers whenever a property changes. This is the trick behind Vue's reactivity system.

JS
function observable(target, onChange) {
  return new Proxy(target, {
    set(obj, prop, value) {
      const ok = Reflect.set(obj, prop, value);
      onChange(prop, value);
      return ok;
    },
  });
}

const state = observable(
  { theme: "light", lang: "en" },
  (key, val) => console.log("changed:", key, val),
);

state.theme = "dark";    // logs: changed: theme dark
Use case 3 — defaults and missing-key handling

JS
const withDefault = (obj, fallback) =>
  new Proxy(obj, {
    get(target, prop) {
      return prop in target ? target[prop] : fallback;
    },
  });

const colors = withDefault({ red: "#f00", blue: "#00f" }, "#888");
colors.red;        // "#f00"
colors.purple;     // "#888"
Use case 4 — debug logs

JS
function trace(name, obj) {
  return new Proxy(obj, {
    get(t, p) {
      console.log(name + "." + String(p) + " read");
      return Reflect.get(t, p);
    },
  });
}

const cart = trace("cart", { items: [], total: 0 });
cart.items;
cart.total;
cart.items read
cart.total read
Pitfalls and limits
  • Proxies are not transparent to ===. The proxy is a different object from its target.

  • Some built-ins (Map, Set, Date, Promise) rely on internal slots and do not work through a proxy without extra handling — the trap returns the right callable, but this must be re-bound to the target.

  • A proxy adds a per-access cost. Fine for app-level state, expensive for inner loops.

  • You cannot proxy a primitive. Wrap it in an object first.

Map/Set inside a proxy
If you proxy a `Map` and someone calls `.get(key)` on the proxy, `this` is the proxy, not the underlying Map. Forward methods with `Reflect.get` plus a manual `.bind(target)`.
When to reach for a Proxy
Most code never needs one. Where they shine: building a small DSL, instrumenting an existing object without changing it, and writing reactive primitives. If you find yourself overriding getters and setters on every property, a Proxy is usually shorter and clearer.
Tip
Pair every trap with the matching `Reflect` call. It keeps your handler honest and ensures defaults still work as the language evolves.