· By John Kavanagh

A Deep Dive into JavaScript's Proxy and Reflect API

Abstract image used to represent JavaScript Proxy and Reflect API
Image by Marija Zaric.

JavaScript's Proxy and Reflect APIs unlock powerful patterns for intercepting and redefining fundamental operations. They're not something that you will reach for every day, but when you do need them, they offer an elegant way to enhance, secure, or observe your objects.

Today, I hope to offer you a brief look into what they are, how they work, and how they fit together.


What is a Proxy?

A Proxy allows us to wrap an object, and then intercept interactions with it ‑ things like reading properties, assigning values, calling functions, or even checking for existence using the in operator. We can also define traps, which are special functions which override default behaviour.

Here's a quick example:

const target = {
  name: 'Ellie',
};

const handler = {
  get(obj, prop) {
    console.log(`Accessed: ${prop}`);
    return prop in obj ? obj[prop] : 'Not found';
  },
};

const proxy = new Proxy(target, handler);

console.log(proxy.name);  // Accessed: name → 'Ellie'
console.log(proxy.age);  // Accessed: age → 'Not found'

In this code, we are setting up a basic JavaScript Proxy that intercepts property access on the target object. When a property is read, the get trap logs which property was accessed and either returns its value (if it exists) or returns the string 'Not found' if it doesn't.

The get trap is called whenever a property is accessed. You can also trap other operations like set, has, deleteProperty, apply, and many more.


What is Reflect?

The Reflect API is a standardised way to perform many of the default operations that Proxy traps override. Think of it as the counterpart to Proxy ‑ it gives us low‑level access to the same internal behaviour that JavaScript uses under the hood.

Here's how you'd use it alongside my previous example:

const handler = {
  get(obj, prop, receiver) {
    console.log(`Accessed: ${prop}`);
    return Reflect.get(obj, prop, receiver);
  },
};

This defines a get trap within a Proxy handler. When a property is accessed on the proxied object, it logs the property name and uses Reflect.get to perform the property lookup, forwarding the receiver supplied to the trap. That receiver becomes this inside a getter. It matters when another object inherits from the proxy: the getter should read that object's values, rather than always using the proxy itself.

So, rather than manually writing logic to return a property or check for its existence, Reflect.get performs the ordinary lookup, including inherited properties and getters. A missing property returns undefined unless a getter or another proxy changes that behaviour.


Why Use Proxy?

Proxy is useful when you need to:

  • Add validation or sanitisation to property assignments.
  • Implement fallback behaviour for missing values.
  • Log or monitor object interactions for debugging.
  • Create reactive data models (as used in Vue 3).
  • Implement access control or virtualised properties.

For a more in‑depth example, here's how we might validate values as they are being set on an object:

const handler = {
  set(obj, prop, value) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('Age must be a number');
    }
    return Reflect.set(obj, prop, value);
  },
};

const user = new Proxy({}, handler);
user.age = 30;  // correct
user.age = 'no';  // throws TypeError

Common Patterns

You'll often see Proxy and Reflect used together; Proxy to intercept, and Reflect to perform the default logic safely.

For example, we can use a Proxy to wrap a class and observe all method calls:

function observe(obj) {
  return new Proxy(obj, {
    get(target, prop, receiver) {
      const value = Reflect.get(target, prop, receiver);
      if (typeof value === 'function') {
        return function (...args) {
          console.log(`Called ${prop} with`, args);
          return value.apply(this, args);
        };
      }
      return value;
    },
  });
}

class Calculator {
  add(a, b) {
    return a + b;
  }
}

const observed = observe(new Calculator());
observed.add(2, 3);  // Logs: Called add with [2, 3]

Limitations and Gotchas

  • Some built‑in methods and getters require internal slots that belong to the original object. A proxy around a Map, for example, does not have the Map's internal storage itself.
  • Class methods that read private fields can also throw if this refers to the proxy rather than the original instance. These receiver requirements need deliberate handling; forwarding every operation through Reflect does not automatically solve them.
  • Once a Proxy is created, there's no way to revoke or "unwrap" it, unless you explicitly use Proxy.revocable.

When to Reach for It

Proxy and Reflect can feel like overkill for everyday use, and that's fine, they aren't built for casual day‑to‑day use. But for libraries, frameworks, or internal tooling, they are incredibly powerful. Vue 3's reactivity system is built on Proxy. If you're using form validators, access control, and/or a dynamic API, you'll often find that they lean on these too.


Wrapping Up

Proxy and Reflect give us the ability to rewire the behaviour of plain JavaScript objects without modifying their shape. When used carefully, we can use them to unlock new possibilities for metaprogramming, observability, and custom logic. They aren't something that you would expect to use every day (or even very often), but they absolutely are worth having in your toolbox.


Have a complex web platform issue?

Tell me what is blocked, what has changed, and what needs to be true after the fix. I'll come back with a practical next step.