Articles

JavaScript Proxy Objects Explained

A JavaScript Proxy wraps an object and intercepts operations like get and set through traps. How traps work, with practical examples and Reflect.

Takina Takina · · 4 min read
A code editor showing a JavaScript component

A JavaScript Proxy wraps an existing object and lets you intercept fundamental operations performed on it — reading a property, setting one, checking if it exists, deleting it — before they reach the real object. Instead of the operation happening directly, it passes through a trap function you define, which can run custom logic, modify the behavior, or forward the operation to the original object unchanged.

const target = { name: "Astro" };

const handler = {
  get(obj, prop) {
    console.log(`reading "${prop}"`);
    return obj[prop];
  },
};

const proxy = new Proxy(target, handler);
proxy.name; // logs: reading "name"  →  returns "Astro"

new Proxy(target, handler) creates the wrapper. target is the real object being wrapped; handler is an object whose methods — the traps — define what happens for each kind of operation. Any trap you don’t define falls through to the target’s default behavior automatically.

Common traps

TrapIntercepts
getReading a property (obj.x, obj["x"])
setAssigning a property (obj.x = 1)
hasThe in operator ("x" in obj)
deletePropertydelete obj.x
ownKeysObject.keys(), for...in, spread
applyCalling the proxy as a function
constructUsing the proxy with new

Each trap receives the target object plus whatever arguments are relevant to that operation, and returns whatever the corresponding native operation should return. A get trap, for instance, receives the target and the property name, and its return value becomes the result of the property read.

A validation example

A common practical use is enforcing invariants on an object without changing how code interacts with it:

function createValidatedUser(data) {
  return new Proxy(data, {
    set(obj, prop, value) {
      if (prop === "age" && typeof value !== "number") {
        throw new TypeError("age must be a number");
      }
      obj[prop] = value;
      return true;
    },
  });
}

const user = createValidatedUser({ name: "Kurumi", age: 24 });
user.age = 25;      // fine
user.age = "old";   // throws TypeError

Code that uses user doesn’t need to know it’s wrapped in a Proxy at all — property assignment looks completely normal. The validation logic runs transparently underneath.

The Reflect API’s role

Notice the set trap above manually does obj[prop] = value; return true; to actually perform the assignment on the target. This works, but it recreates default behavior by hand — for more advanced traps, that gets error-prone quickly. Reflect provides methods that mirror every trap and correctly forward operations to the target with the right semantics, including edge cases around prototypes and property descriptors that are easy to get subtly wrong by hand:

const handler = {
  set(obj, prop, value, receiver) {
    console.log(`setting "${prop}" to`, value);
    return Reflect.set(obj, prop, value, receiver);
  },
};

Proxy and Reflect were introduced as a pair for exactly this reason — Reflect gives you a reliable way to fall through to default behavior from inside a trap.

Real-world use cases

  • Reactivity systems. Frontend frameworks that track which parts of an object were read and which were written use get and set traps to know when to re-render — the same underlying pattern that fine-grained signal-based state systems build on, just implemented at the object level rather than through explicit signal functions.
  • Default values. A get trap can return a computed default for any property that isn’t already set on the target, instead of undefined.
  • API shims and logging. Wrapping an object to log every access or mutation is useful for debugging without modifying the object’s actual implementation.
  • Immutability enforcement. A set and deleteProperty trap that always returns false (in non-strict contexts) or throws (in strict mode) can make an object behave as read-only, layered on top of an object that wasn’t designed to be immutable.

Revocable proxies

A related but less commonly used constructor, Proxy.revocable(target, handler), creates a proxy alongside a revoke function that permanently disables it. Calling revoke() makes every subsequent operation on the proxy throw a TypeError, regardless of which trap would normally handle it:

const { proxy, revoke } = Proxy.revocable({ secret: 42 }, {});
proxy.secret; // 42
revoke();
proxy.secret; // throws TypeError

This is useful for handing out access to an object that should later be cut off entirely — for example, giving a third-party module a proxy to some internal state, then revoking it once that module’s task is done, without needing to track down every reference to the original object.

What a Proxy can’t do

A Proxy intercepts operations performed on the object — it can’t retroactively change code elsewhere that already holds a direct reference to the unwrapped target rather than the proxy. And because trap functions run on every intercepted operation, a Proxy used on a hot path — like an object read thousands of times in a tight loop — carries measurable overhead compared to direct property access, so it’s a poor fit for performance-critical inner loops even though it’s negligible for typical application-level object access.

Proxies also don’t change how closures work — a trap function closes over its surrounding scope exactly like any other function, which is often how validation or logging logic inside a handler gets access to state outside the target object itself.

The takeaway

A JavaScript Proxy wraps a target object and routes fundamental operations — property reads, writes, existence checks, deletions — through trap functions you define, falling through to default behavior for any trap you don’t implement. Pair it with Reflect to forward operations correctly rather than reimplementing default semantics by hand. It’s the mechanism behind reactivity systems, validation layers, and API shims, but it comes with real per-operation overhead, so reach for it where the interception is the point, not on data structures accessed in tight, performance-sensitive loops.

Takina Takina · · 4 min read

TypeScript Abstract Classes, Explained

Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.

#TypeScript #JavaScript #Web Development
Takina Takina · · 4 min read

What Is a Lockfile? Reproducible Dependency Installs

A lockfile records the exact dependency versions your package manager resolved, so every install — from your laptop to CI — reproduces the same tree.

#JavaScript #Web Development #Developer Tools
Takina Takina · · 3 min read

TypeScript's never Type, Explained

never represents values that can't exist — it marks unreachable code, exhaustive switches, and functions that always throw or loop forever.

#TypeScript #JavaScript #Web Development