Skip to content
JavaScript js advanced 5 min read

Proxy & Reflect

A Proxy wraps an object and lets you intercept the fundamental operations performed on it — reading a property, writing one, checking existence, deleting, and more. Each interception is called a trap. The Reflect API is its natural companion: a collection of static methods that perform those same fundamental operations as plain functions, giving you a clean way to invoke the default behavior from inside a trap. Together they power validation layers, reactive frameworks like Vue 3, smart defaults, and transparent logging — all without touching the original object.

Creating a proxy

A proxy is built from two arguments: the target (the object being wrapped) and a handler (an object whose methods are the traps). Operations on the proxy are forwarded to the matching trap; any operation without a trap falls through to the target unchanged.

const target = { message: "hello" };

const proxy = new Proxy(target, {
  get(obj, prop, receiver) {
    return prop in obj ? obj[prop] : `<no ${String(prop)}>`;
  },
});

console.log(proxy.message);
console.log(proxy.missing);

Output:

hello
<no missing>

The proxy is a transparent stand-in: from the outside it behaves like the target, but every access flows through your handler first.

Common traps

Each trap mirrors a low-level object operation. The most-used ones:

TrapTriggered bySignature
getreading obj.x, obj["x"](target, prop, receiver)
setwriting obj.x = v(target, prop, value, receiver)
has"x" in obj(target, prop)
deletePropertydelete obj.x(target, prop)
ownKeysObject.keys, for...in(target)
applycalling a function proxy(target, thisArg, args)
constructnew on a function proxy(target, args, newTarget)

The set trap must return a truthy value to signal success; returning false (or nothing) throws a TypeError in strict mode. The has trap controls what the in operator reports.

const config = new Proxy({ debug: false }, {
  has(target, prop) {
    // Hide any property that starts with an underscore.
    if (typeof prop === "string" && prop.startsWith("_")) return false;
    return prop in target;
  },
  deleteProperty(target, prop) {
    if (prop === "debug") throw new Error("debug cannot be removed");
    delete target[prop];
    return true;
  },
});

config._secret = 42;
console.log("_secret" in config);
console.log("debug" in config);

Output:

false
true

Reflect: the default-behavior toolkit

Inside a trap you often want to do the normal thing — but with a hook around it. Writing target[prop] = value works for simple cases, yet it ignores the receiver and can break inheritance with getters/setters. Reflect solves this: every trap has a one-to-one Reflect method with the same arguments, so forwarding is trivial and correct.

Reflect methodEquivalent operation
Reflect.get(t, k, r)t[k]
Reflect.set(t, k, v, r)t[k] = v
Reflect.has(t, k)k in t
Reflect.deleteProperty(t, k)delete t[k]
Reflect.ownKeys(t)Object.keys + symbols

Forwarding the receiver matters when the proxy sits on a prototype and a getter references this. Reflect.get(target, prop, receiver) ensures this points at the proxy, not the raw target.

Always prefer Reflect.set(target, prop, value, receiver) over target[prop] = value inside a set trap. The Reflect form preserves the correct receiver and returns the boolean success flag your trap is required to return.

Use case: validation

Proxies make great validation gates because they intercept assignment before it lands.

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

const user = validatedUser();
user.age = 30;
console.log(user.age);

try {
  user.age = -5;
} catch (err) {
  console.log(err.message);
}

Output:

30
age must be a non-negative integer

Use case: reactivity and logging

Vue 3’s reactivity system is built on proxies: a get trap records which effect read a property (dependency tracking), and a set trap re-runs those effects. A simplified version shows the shape of the idea.

function reactive(obj, onChange) {
  return new Proxy(obj, {
    get(target, prop, receiver) {
      console.log(`read ${String(prop)}`);
      return Reflect.get(target, prop, receiver);
    },
    set(target, prop, value, receiver) {
      const ok = Reflect.set(target, prop, value, receiver);
      onChange(prop, value);
      return ok;
    },
  });
}

const state = reactive({ count: 0 }, (prop, value) =>
  console.log(`changed ${prop} -> ${value}`)
);

state.count;
state.count = 1;

Output:

read count
changed count -> 1

The same get/set logging pattern is invaluable for debugging — drop a proxy around any object and watch every interaction without editing the code that uses it.

Use case: smart defaults

A get trap can synthesize values for missing keys, turning a plain object into a defaulting dictionary.

const counts = new Proxy({}, {
  get: (target, prop) => (prop in target ? target[prop] : 0),
});

console.log(counts.apples);
counts.apples = 3;
console.log(counts.apples);

Output:

0
3

Revocable proxies

Proxy.revocable(target, handler) returns { proxy, revoke }. After calling revoke(), any operation on the proxy throws — useful for handing out access that you can later cut off (e.g. tokens or sandboxed APIs).

Best practices

  • Use Reflect methods inside traps to invoke default behavior; never hand-roll operations that drop the receiver.
  • Return the correct value from each trap — set, deleteProperty, and defineProperty must return a boolean, or strict mode throws.
  • Keep traps fast and side-effect-light; they run on every matching operation and can hide expensive work behind innocent-looking property access.
  • Respect proxy invariants — you cannot, for example, report a non-configurable, non-writable property as a different value, or the engine throws a TypeError.
  • Reach for Proxy.revocable when you need to invalidate access cleanly rather than leaking a live reference.
  • Avoid proxying performance-critical hot paths; the indirection has real overhead compared to direct property access.
Last updated June 1, 2026
Was this helpful?