A Deep Dive into JavaScript's Proxy and Reflect API

Hero image for A Deep Dive into JavaScript's Proxy and Reflect API. Image by Marija Zaric.
Hero image for 'A Deep Dive into JavaScript's 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 lowlevel 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) {    console.log(`Accessed: ${prop}`);    return Reflect.get(obj, prop, proxy);  },};

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 safely retrieve the value from the original object, passing in the proxy itself as the receiver to maintain correct context.

So, rather than manually writing logic to return a property or check for its existence, Reflect.get does it for you in a very safe and predictable way.


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 indepth 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;  // correctuser.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

  • Proxies don't play nicely with some builtin operations, especially in older environments.
  • If you're proxying complex objects (like DOM elements or classes), behaviour might be inconsistent.
  • 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 daytoday 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.


Looking for technical direction?

I support teams that need senior judgement on React, Next.js, headless CMS architecture, performance, migrations, and technical SEO.