Using Reflect and Proxy Together in JavaScript

Proxy traps intercept operations, but Reflect is what makes them behave correctly. Learn why Reflect belongs inside every trap and what breaks when you skip it.

8 min read

Reflect and Proxy were added to JavaScript in the same version for a reason: a Proxy trap intercepts an operation, and the matching Reflect method is that operation's exact default behavior. Writing a trap without Reflect usually still works for simple cases, but it quietly breaks once inheritance or accessor properties get involved.

This article assumes familiarity with Proxy traps already. For the full trap list and basic use, see advanced JavaScript proxies.

For the full method reference on its own, see the JavaScript Reflect API. Here, the focus is only on why the two are used together, starting with a trap that logs every read before forwarding it:

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

The get trap fires first, and the matching Reflect call performs the actual read afterward. Every argument the trap received gets passed straight through to Reflect, which is the pattern used throughout this article.

What Breaks Without Reflect

It is tempting to skip Reflect and access the target directly, since it looks simpler and produces the same result for plain objects with no inheritance involved:

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

This works here because the target has no getters and nothing else inherits from the proxy. The difference appears once a getter is involved and the proxy is used as another object's prototype, as shown in the next example:

javascriptjavascript
const target = {
  get greeting() {
    return `Hello, ${this.name}`;
  },
};
 
const handler = {
  get(target, prop) {
    return target[prop];
  },
};
 
const proxy = new Proxy(target, handler);
const user = Object.create(proxy);
user.name = "Diego";
 
console.log(user.greeting);
// Hello, undefined

The word "this" inside the getter resolves to the target, not the user object, because reading the property directly runs the getter with the target as the receiver. The target has no name property of its own, which is why the result comes back undefined instead of the expected greeting.

Fixing It with the Receiver Argument

The trap actually receives a third argument representing the object the property access originally started on. Passing that argument through Reflect fixes the binding, as shown by changing only the get trap below:

javascriptjavascript
const target = {
  get greeting() {
    return `Hello, ${this.name}`;
  },
};
 
const handler = {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver);
  },
};
 
const proxy = new Proxy(target, handler);
const user = Object.create(proxy);
user.name = "Diego";
 
console.log(user.greeting);
// Hello, Diego

Forwarding through Reflect with the receiver argument runs the getter with the user object as this, instead of the target. That single argument is exactly why the get and set methods on Reflect both accept a receiver parameter that plain property access has no equivalent for.

The diagram below traces how that receiver argument travels from the original property access all the way into the getter itself.

Receiver flow through Reflect.get

The receiver travels from the original access all the way into the getter's this binding. Without passing it through, that chain breaks at the forwarding step, and the getter falls back to using the target as this instead of the object the reader actually started from.

The Same Pattern for set

A set trap needs the same treatment, since a setter can also depend on this matching the object the assignment started on, not the raw target underneath the proxy:

javascriptjavascript
const target = {
  set name(value) {
    this._name = value.toUpperCase();
  },
};
 
const handler = {
  set(target, prop, value, receiver) {
    return Reflect.set(target, prop, value, receiver);
  },
};
 
const proxy = new Proxy(target, handler);
const user = Object.create(proxy);
user.name = "sam";
 
console.log(user._name);
// SAM

The set method on Reflect returns true or false to report success, which matches what a set trap is required to return. Returning that call directly satisfies the requirement automatically, instead of writing a manual return statement that might get the result wrong for a failed assignment.

When Reflect Is Optional

Not every trap needs Reflect. Traps that implement entirely custom logic instead of forwarding to the target, like the membership check below, do not need it at all:

javascriptjavascript
const handler = {
  has(target, prop) {
    if (prop.startsWith("_")) return false;
    return prop in target;
  },
};

This trap deliberately hides private-looking keys instead of forwarding every check to Reflect, which is fine since custom behavior is the entire point of writing it. Reflect matters most in traps meant to mostly keep default behavior intact while adding a small piece of logic around it, such as logging or validation.

Practical Rule

Default every Proxy trap to calling the matching Reflect method with all of the trap's arguments passed through, including the receiver. Only skip Reflect when the trap is meant to return something entirely custom, such as blocking access or hiding a property. This keeps proxies behaving like normal objects in every case except the one intentionally changed.

Rune AI

Rune AI

Key Insights

  • Every core Proxy trap has a matching Reflect method with the same name and argument order.
  • The get and set methods on Reflect accept a receiver argument that keeps this correct inside getters and setters during inheritance.
  • Accessing the target object directly inside a trap works for simple objects but breaks once the proxy is used as another object's prototype.
  • Reflect methods return true or false for operations that can fail, which is exactly what the matching Proxy traps are required to return.
  • Skipping Reflect is fine for small custom logic in traps like has, but risky for traps that should mostly forward to default behavior, like get and set.
RunePowered by Rune AI

Frequently Asked Questions

Can I write a Proxy trap without Reflect at all?

Yes, for simple cases where you access the target object directly instead of going through Reflect. It works until the proxy is used as a prototype for another object, at which point skipping the receiver argument produces incorrect this binding inside getters.

Do I need Reflect for every single trap?

No. Traps like has or ownKeys often just need to return a plain value with custom logic. Reflect is essential specifically when you want to forward to the target's default behavior, which is the majority of traps in most Proxy handlers.

Conclusion

Reflect and Proxy are designed as a pair. Proxy intercepts an operation, and Reflect provides its exact default implementation, including the details a proxy trap alone would get wrong, like receiver binding during inheritance. Accessing the target directly works for simple objects and breaks once a proxy sits in a prototype chain.