The Glitched Goblet Logo

The Glitched Goblet

Where Magic Meets Technology

Proxy: The Most Dangerous JavaScript Feature You’ve Never Used

March 31, 2025

"With great power comes the ability to absolutely wreck your own code."

Intro

JavaScript is like a Multi-Tool. It has a specific tool for everything, but mostly we ignore the weird looking ones and just use the pliers and sometimes the screwdriver. I mean, it does solve most of our problems, right?

For example, whenever a bug comes along, the goto is console.log. If that doesn't work, use more console.log statements. When that doesn't work we either cry or use the debugger.

But what if I were to tell you that there is a secret third option? A middle-ground between console.log and debugger that can help you debug your code in a more elegant way. Enter Proxy.

So, what is Proxy?

"The Proxy object enables you to create a proxy for another object, which can intercept and redefine fundamental operations for that object." - MDN

But what does that mean? JavaScript’s Proxy object is like middleware for your Objects. It lets you dig into various operations like property access, assignment, enumeration, function calls. You can then intercept them, modify them, or just watch them happen.

This is what it looks like:

// The object I wanna spy on
const target = { message: 'Hello World' }

// The watcher that'll do the spying
const handler = {
  get(target, prop) {
    console.log(`Accessed: ${prop}`)
    return target[prop]
  },
}

// Proxy connects the two
const proxy = new Proxy(target, handler)
console.log(proxy.message) // Logs "Accessed: message" then "Hello"

The Proxy takes two things:

  1. A target object (the thing being watched)
  2. A handler object (the thing doing the watching)

Every time something touches the proxy, your handler can intercept it. This is known as a trap, which I love. But remember, with great power... comes an even greater ability to make more bugs. So let's see how we can use this power for good.

Use Case #1: Debug Utility

Create a generic handler object that just logs events. Having difficulty figuring out an objects values and want to spy on it? Just import the utility and wrap it in a Proxy to log every read/write. Now you don't have to worry about adding/removing log statements throughout your code.

const debugProxy = (obj) =>
  new Proxy(obj, {
    get(target, prop) {
      console.log(`[GET] ${String(prop)}`)
      return Reflect.get(...arguments)
    },
    set(target, prop, value) {
      console.log(`[SET] ${String(prop)} = ${value}`)

      return Reflect.set(...arguments)
    },
  })

const user = debugProxy({ name: 'Kaemon', level: 99 })
user.name // [GET] name
user.level = 100 // [SET] level = 100

Perfect for hunting down rogue state mutations or just spying on your app like a digital goblin. Speaking of mutations, you may have noticed that I've used Reflect in the handler. This post won't go into detail on Reflect, all you need to know is that it provides default behavior for the traps. You can use it to call the default behavior of the operation you're intercepting. This prevents mutation and allows safe spying without creating even more bugs.

Use Case #2: Auto-default Fallbacks

Wanna take Proxy even further and use it as part of your everyday code? You can give your objects default responses! No more undefined errors when you forget to set a value.

const config = new Proxy(
  {},
  {
    get: (_, prop) => `Missing config: ${String(prop)}`,
  },
)

console.log(config.apiKey) // "Missing config: apiKey"

Now you can spot missing values at a glance, without your app crashing halfway through rendering. Better yet, you can use it to set default values for your objects.

Want it to fall back to specific defaults instead of generic warnings? You can combine Proxy with a defaults object like this:

const defaults = { name: 'Anonymous', age: 0 }
const userData = new Proxy(
  {},
  {
    get: (target, prop) => (prop in target ? target[prop] : defaults[prop]),
  },
)

Now you can access userData without worrying about missing properties. It'll just fall back to the defaults you set. No more undefined errors, no more tears.

Use Case #3: Validation on Set

Why wait for bugs when you can yell at yourself in real time? Use Proxy to validate your object properties as they're set.

const userData = new Proxy(
  {},
  {
    set(target, prop, value) {
      if (prop === 'age' && typeof value !== 'number') {
        throw new TypeError('Age must be a number')
      }

      return Reflect.set(...arguments)
    },
  },
)

userData.age = 30 // 👍
userData.age = 'thirty' // 💥 TypeError

You can even combine this with forms to validate user input without a mountain of if-checks.

But Wait... Should You?

There are endless possibilities with Proxy, you can even use it to build your own API clients, or a reactive state management system. As you can see, Proxy is powerful. But like most powerful tools, it’s easy to abuse.

You can easily make your life even harder by overusing Proxy. Here are some things to consider before you go all-in:

  • Harder to debug when something breaks inside the handler.
  • Mental overhead for devs unfamiliar with Proxy.

You should use it when you want to:

  • Debug object access without adding tons of console.log statements.
  • Prevent undefined errors with default values.
  • Validate object properties in real-time.

TL;DR

  • Use Proxy to log and spy on objects.
  • Give your objects default values to avoid undefined.
  • Validate values as they’re set.
  • Use responsibly or suffer the bugs.

Final Thoughts

Proxy is a powerful tool that can help you debug your code in a more elegant way. But like any tool, it has its place. This shouldn't be the new console.log, but it can be a powerful ally in your debugging arsenal.

Go forth and intercept, my chaotic coder. Just remember to use your powers for good, not evil! Got a cursed or clever Proxy trick? If you're interested in reading more of my thoughts, check out my blog at The Glitched Goblet.

<!-- psst... try console.log(window.glitch) on my blog... -->