skip to content

In JavaScript, what does `new Proxy(target, handler)` create, what is a trap, and what happens for an operation the handler defines no trap for?

level: juniorimportance: should knowfreq 40%

answer

  1. wrapper object, plus an intercept table
  2. one handler method per internal operation
  3. omitted entries do nothing special
  4. thirteen of them, target always first

basics

~20 s

new Proxy(target, handler) returns a new object that routes fundamental operations — property reads, writes, deletes, key listing, calls — to matching handler methods called traps. Any operation whose trap is missing falls through to the target unchanged.

solid answer

~40 s

`new Proxy(target, handler)` creates an exotic object that sits in front of `target`. Every fundamental operation on it — the internal methods the spec defines, such as [[Get]], [[Set]], [[HasProperty]], [[Delete]], [[OwnPropertyKeys]], [[DefineOwnProperty]], [[Call]] — is routed to the same-named method on `handler`. Those handler methods are the traps: `get`, `set`, `has`, `deleteProperty`, `ownKeys`, `getOwnPropertyDescriptor`, `defineProperty`, `apply`, `construct` and a few more, thirteen in total. The handler is an ordinary object, so a trap you omit simply is not there and the engine performs the default operation on the target — which is why `new Proxy(obj, {})` behaves almost exactly like `obj`. Both arguments must be objects. Callability comes from the target: `apply` and `construct` only fire if the target is itself a function, and `typeof proxy` reports the target's kind.

code

javascript · 13 lines
javascript
const target = { a: 1 };

const p = new Proxy(target, {
  get(t, key, receiver) {
    console.log('read', key);
    return t[key];
  },
});

console.log(p.a);      // logs "read a", then 1
p.b = 2;               // no set trap: writes straight through
console.log(target.b); // 2 — same object underneath
console.log(target.a); // 1 — reading the target directly logs nothing

go deeper

for a junior

Be able to write new Proxy(target, handler) from memory, name three traps (get, set, has), and say plainly that a missing trap means the operation just happens on the target.

for a middle

Explain the mapping from syntax to internal method to trap — 'x' in p reaching has, delete p.x reaching deleteProperty — and know which traps must return a boolean.

for a senior

Show where the abstraction stops: no trap for === or typeof, method calls only surface as a get, keys arrive as strings, and code holding the raw target bypasses the wrapper entirely.

for a principal

Be ready to argue when putting a proxy on a shared object is worth it at all, given that every reader of that object now has behaviour it cannot see in its own source, and that the raw target usually leaks somewhere.

## What `new Proxy` produces A proxy is an *exotic object* — an object whose spec-level behaviour is deliberately not the ordinary one. `new Proxy(target, handler)` stores two references, the target and the handler, and returns a brand-new object. It is not a copy of the target and it does not change the target. Code that still holds the original `target` can read and write it directly, and the handler never sees any of that; only work done *through the proxy* is intercepted. Both arguments must be objects. `new Proxy(1, {})` and `new Proxy({}, null)` both throw a `TypeError`. ## Fundamental operations and traps ECMAScript defines every object in terms of a fixed set of *internal methods*: `[[Get]]`, `[[Set]]`, `[[HasProperty]]`, `[[Delete]]`, `[[OwnPropertyKeys]]`, `[[GetOwnProperty]]`, `[[DefineOwnProperty]]`, `[[GetPrototypeOf]]`, `[[SetPrototypeOf]]`, `[[IsExtensible]]`, `[[PreventExtensions]]`, and, for functions, `[[Call]]` and `[[Construct]]`. Syntax is specified on top of those: `obj.x` is `[[Get]]`, `'x' in obj` is `[[HasProperty]]`, `delete obj.x` is `[[Delete]]`, `Object.keys(obj)` starts at `[[OwnPropertyKeys]]`. A proxy re-implements each internal method as one rule: *if the handler has a method of the corresponding name, call it; otherwise perform the default operation on the target.* Those handler methods are the traps, one per internal method — thirteen of them: ```js const handler = { get(target, key, receiver) {}, set(target, key, value, receiver) {}, has(target, key) {}, deleteProperty(target, key) {}, ownKeys(target) {}, getOwnPropertyDescriptor(target, key) {}, defineProperty(target, key, descriptor) {}, getPrototypeOf(target) {}, setPrototypeOf(target, proto) {}, isExtensible(target) {}, preventExtensions(target) {}, apply(target, thisArg, argsList) {}, construct(target, argsList, newTarget) {}, }; ``` Every trap receives the target as its first argument, because the trap is a plain function with no privileged access — if it wants the real value it has to go and get it. ## There is no trap for what the language does not model as an internal method This is the boundary candidates most often get wrong. There is no trap for `===`, none for `typeof`, none for a `for` loop, and no separate trap for a method call: `p.f(1)` is a `[[Get]]` of `"f"` on the proxy followed by an ordinary call of whatever came back, so you see a `get` trap and nothing else. Property keys always arrive as strings or symbols, never numbers — `p[0]` reaches the `get` trap as the string `"0"`. ## Missing traps mean default behaviour, not failure An empty handler is legal and useful as a starting point: ```js const obj = { a: 1 }; const p = new Proxy(obj, {}); p.a; // 1 — no get trap, so the default [[Get]] runs on obj p.b = 2; // writes b onto obj Object.keys(p); // ['a', 'b'] obj.b; // 2 — same object underneath ``` That is the mental model to keep: a proxy is transparent by default, and each trap you add is one place where you take the operation over. ## What traps must return Some traps have a contract on their result. `set`, `has`, `deleteProperty`, `defineProperty`, `preventExtensions` and `setPrototypeOf` return a boolean meaning "the operation succeeded"; the result is coerced to boolean, and in strict mode — which module code and class bodies always are — a `set` trap returning `false` makes the assignment throw a `TypeError` rather than fail silently. The `construct` trap must return an object or the `new` expression throws. `ownKeys` must return an array-like of strings and symbols. Returning nothing from a trap that needs a value means returning `undefined`, which is a common source of "my proxy broke everything" bugs. ## Callability comes from the target A proxy is callable if and only if its target is. Defining an `apply` trap over a plain object does not make the proxy callable — calling it throws a `TypeError`, and the trap never runs. Wrap a function instead: ```js const logged = new Proxy(function sum(a, b) { return a + b; }, { apply(target, thisArg, args) { console.log('called with', args); return target.apply(thisArg, args); }, }); logged(2, 3); // logs, then 5 typeof logged; // 'function' ``` The same rule applies to `construct`: the target must be a constructor. And because callability is fixed when the proxy is created, `typeof` never invokes a trap — it always reports the kind the target had at construction time.

  • If a proxy has no `get` trap but does have a `set` trap, what happens on `p.x = 1` followed by `p.x`?
    The assignment runs your `set` trap, and nothing else happens automatically — if the trap does not write to the target and returns a truthy value, the write is silently swallowed. The later read has no trap, so it performs the default `[[Get]]` on the target and yields whatever is really there, very likely `undefined`.
  • Why does `p.greet()` show a `get` trap firing but no dedicated call trap?
    Because a method call is not a fundamental operation. `p.greet()` is specified as `[[Get]]` of `"greet"` on the proxy, then an ordinary call of the returned function with `p` as `this`. The function itself is not a proxy, so nothing intercepts the invocation. To trap the call you have to wrap the returned function in its own proxy with an `apply` trap.
  • Can the handler object be changed after the proxy is created?
    Yes — the proxy keeps a reference to the handler object, not a snapshot of its methods, so adding, removing or reassigning a trap on that object changes the proxy's behaviour immediately. That is convenient and also a hazard: anyone holding the handler can silently re-arm the wrapper afterwards.

saying these in an interview costs you the question

  • Says a Proxy modifies or replaces the original target object
  • Thinks an empty handler blocks or breaks every operation
  • Expects a dedicated trap for method calls or for ===
  • Believes an apply trap makes a plain-object proxy callable
  • Assumes changes made directly on the target still hit the handler

context