skip to content

Proxy in Practice

Where proxies genuinely earn their keep — reactive state, validation, defaults, negative indices, dynamic API clients — and what they cost. Follow-ups target performance overhead and the things a Proxy cannot transparently wrap.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In JavaScript, what can wrapping an object in a Proxy do that plain getters and setters cannot, and which real use cases does that unlock?

level: middleimportance: must knowfreq 55%

answer

  1. one wrapper for the whole object
  2. keys that do not exist yet
  3. also delete, in, key listing
  4. reactivity, validation, defaults, dynamic clients
  5. a handler call per operation

basics

~20 s

A Proxy intercepts operations on property names that do not exist yet, plus deletes, in-checks, key enumeration and calls, for any target including arrays and functions. Getters and setters only cover properties you declared in advance.

solid answer

~50 s

The difference is coverage. A getter/setter pair has to be attached to a property name you already know, and it only sees reads and writes of that one name. A Proxy sits in front of the whole object, so one handler answers for keys nobody has defined, and it can also intercept `delete`, the `in` operator, key listing, and calls when the target is a function. That is what makes the practical uses possible: reactive state that notices brand-new keys (Vue 3's `reactive()` and Immer's drafts are built on it), one central place to validate every assignment, defaults for missing keys, negative array indices, access logging, and API clients whose methods are invented from the property name. The price is a handler call on every operation, and the proxy being a different object from the target.

go deeper

for a junior

Be able to say what new Proxy(target, handler) produces: a stand-in object whose reads and writes can run your code first, with anything you do not intercept passed through to the target.

for a middle

Explain the coverage difference concretely — a handler covers keys that do not exist yet plus delete, in and key listing — and name two uses that depend on it, such as reactive state and central validation.

for a senior

Show judgment about where the wrapper belongs: keep the raw target from escaping, expect the per-operation cost, and know that the proxy is a distinct object, so identity checks and objects with internal slots need care.

for a principal

Own the API decision. Argue when a Proxy's hidden control flow is worth the debuggability and performance loss versus explicit accessors or plain functions, and set rules for where proxied values may cross module or worker boundaries.

## What a Proxy actually wraps `new Proxy(target, handler)` returns a new object that stands in front of `target`. Every fundamental operation on the proxy — reading a property, writing one, deleting one, asking `in`, listing keys, calling it — is routed to the matching method on `handler` if one exists, and otherwise performed on the target unchanged. An empty handler therefore produces something that behaves almost exactly like the target. The key word is *operation*. An accessor property (a getter/setter pair) is bound to one name that must exist before anything can be intercepted, and it only covers get and set. A Proxy is bound to the object, not to a name, so it can answer for names that were never declared and for operations that are not property reads at all. ```js const counted = new Proxy({}, { get(target, key, receiver) { if (!Reflect.has(target, key)) return 0; // default for unknown keys return Reflect.get(target, key, receiver); } }); counted.anythingAtAll; // 0 — no property was ever defined ``` ## The use cases that genuinely need it **Reactive state.** A store must know when *any* key is read (to record a dependency) and when *any* key is written or deleted (to invalidate). Installing accessors at construction time can only cover the keys present then; adding a key later, or deleting one, is invisible. A Proxy covers the whole object for its whole life, which is why Vue 3's `reactive()` and Immer's copy-on-write drafts are Proxy-based. **Validation and invariants in one place.** A `set` trap runs for every assignment, so range checks, type checks and required-field rules live in one handler instead of being duplicated per property. In strict-mode code — which includes all ES modules — a `set` trap that returns `false` makes the assignment throw a `TypeError`, though throwing your own error with a useful message is friendlier. **Defaults and safe lookup.** A `get` trap that returns a fallback turns missing-key `undefined` into a sensible value; the inverse — throwing on an unknown key — turns a typo in a config object from a silent `undefined` into an immediate error. **Negative array indices and other index tricks.** `arr[-1]` is a plain property read of the key `"-1"`; a `get` trap can translate it into `length - 1`. **Dynamic API clients.** `api.getUser` need not exist. A `get` trap can manufacture a function from the property name, so a client library covers an entire endpoint surface without generating code for it. **Access logging and tracing.** Wrapping a service object in a logging proxy records every call site during debugging without editing the object. ## What it costs Every intercepted operation becomes a call into your handler, so an engine cannot use the inline caches it would use for a plain property read. That is fine for configuration objects and UI state touched at human speed, and a real problem in a loop touching millions of elements. There is also a transparency cost: the proxy is not the target. `proxy !== target`, a lookup in a `Map` or `WeakMap` keyed by the target will not find the proxy, methods that need internal slots (`Map.prototype.get`, `Date.prototype.getTime`, DOM node methods) throw when invoked with the proxy as `this`, and methods reading `#private` fields throw for the same reason. And anyone still holding the raw target writes straight past your traps — a reactivity or validation layer only works if the proxy is the *only* reference that circulates. ## When not to reach for one If you know the property names up front and only need get/set behaviour, accessors are simpler, faster and easier to debug. If you need to freeze behaviour rather than observe it, `Object.freeze` says so directly. And a Proxy adds hidden control flow: a reader of `user.name = x` has no syntactic hint that arbitrary code runs. Use it where the dynamism is the point, and keep the wrapped surface small and documented.

  • Your validating handler rejects a bad value by returning false from the set trap. What does the assignment actually do?
    In strict-mode code — which includes every ES module and class body — the assignment throws a `TypeError` saying the trap returned falsish. In sloppy mode it fails silently, which is worse than useless for validation. Prefer throwing your own error inside the trap so the message names the property and the rule it broke.
  • Why do proxy-based reactivity layers beat installing accessors on every property at initialization?
    Accessors installed once cover only the keys that existed then. Adding a key, deleting a key, or writing a new array index is invisible, so libraries built that way needed explicit set/delete helpers and could not track array length changes well. A Proxy intercepts the whole object for its whole life, including keys invented later.
  • Someone kept a reference to the raw target and assigns to it directly. What happens to your traps?
    Nothing runs. The proxy and the target are two objects sharing state, not one object with a filter; only operations performed *on the proxy* hit the handler. So a validation or reactivity layer must ensure the proxy is the only reference that escapes — create the target inline, or hide it in a closure or module scope.

saying these in an interview costs you the question

  • Says a Proxy makes the target read-only or immutable
  • Thinks the handler must implement every trap
  • Claims getters can intercept properties that do not exist
  • Assumes writes to the raw target still trigger traps
  • Believes proxying is free at run time
  • Says a Proxy and its target are the same object

context

open as a page

You want arr[-1] to return the last element by wrapping a JavaScript array in a Proxy. What must the get handler do with the property key it receives, and what must it still forward?

level: middleimportance: should knowfreq 35%

basics

~20 s

Property keys reach a get trap as strings or symbols, never numbers, so the handler must convert the key to a number, verify it is a negative integer, and translate it to length plus that index. Everything else must be forwarded unchanged.

open as a page

What does using a JavaScript Proxy cost at run time compared with a plain object, and how do you decide whether that cost matters for a given piece of code?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Every intercepted operation on a proxy becomes a call into a handler function, so engines cannot use the inline caches that make plain property reads nearly free. Expect a large relative slowdown per access and judge it by how many accesses the code performs.

open as a page

A colleague wraps existing objects in a JavaScript Proxy expecting a drop-in replacement. Which kinds of objects stop working through the proxy, and why?

level: seniorimportance: should knowfreq 35%

basics

~20 s

A Proxy is a different object from its target, so anything keyed to the target's identity breaks: methods reading #private fields, built-ins with internal slots such as Map, Set, Date, typed arrays and DOM nodes, and lookups in collections keyed by the original object.

open as a page

You are building a reactive state layer on top of JavaScript Proxy. How do you decide between wrapping the object graph deeply and wrapping only the top level, and what does the deep option force you to build?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Deep wrapping observes nested mutations without ceremony but requires lazy per-access wrapping, a target-to-proxy cache for stable identity, special handling for collections and class instances, and an unwrap hatch. Shallow wrapping is cheap and predictable but pushes explicit replacement onto callers.

open as a page