skip to content

In JavaScript, a Proxy's `get` trap is called with `(target, property, receiver)`. What is `receiver`, and give a case where it is not the proxy itself.

level: seniorimportance: should knowfreq 28%

answer

  1. two objects per lookup, not one
  2. where the search began, not where it landed
  3. prototype chains split them apart
  4. getters need it to see the right state

basics

~20 s

receiver is the object the property access actually started on, and the value that becomes this inside any getter. It is the proxy for a direct access, but the inheriting object when the proxy sits on that object's prototype chain.

solid answer

~50 s

`receiver` is the object the lookup began on — in spec terms, the `[[Get]]` receiver, and the value that will be `this` for any accessor invoked as a result. For a plain `p.x` it is the proxy. It differs the moment the proxy is somewhere on another object's prototype chain: with `const child = Object.create(p)`, reading `child.x` walks up, finds the proxy, and fires its `get` trap with `target` being the wrapped object but `receiver` being `child`. The `set` trap takes the same value as its fourth argument. It matters because forwarding with a bare `target[property]` throws the receiver away, so a getter on the target runs with `this` pointing at the target instead of the real object — and in a `set` trap the same shortcut writes the property onto the target rather than onto `child`. `Reflect.get`/`Reflect.set` take a receiver argument precisely so you can forward it.

code

javascript · 17 lines
javascript
const store = {};

const leaky = new Proxy(store, {
  set(t, key, value) { t[key] = value; return true; },
});
const a = Object.create(leaky);
const b = Object.create(leaky);
a.count = 1;
console.log(b.count);   // 1 — written onto the shared target

const correct = new Proxy({}, {
  set(t, key, value, receiver) { return Reflect.set(t, key, value, receiver); },
});
const c = Object.create(correct);
const d = Object.create(correct);
c.count = 1;
console.log(d.count);   // undefined — each child owns its property

go deeper

for a junior

Recall the get trap's signature (target, property, receiver) and that the third argument is the object the read started on, which is normally just the proxy.

for a middle

Explain the prototype-chain case concretely: Object.create(proxy) makes the trap fire with receiver set to the child, while target is still the wrapped object.

for a senior

Demonstrate the two production consequences — accessors running with this bound to the target, and inherited assignments landing on a shared object instead of the instance — and show the forwarding that fixes both.

for a principal

Own the guidance that any proxy which might end up on a prototype chain has a receiver contract to honour, and be able to explain why a wrapper that is not identity-correct will eventually corrupt shared state in ways that look like a data bug, not a metaprogramming bug.

## What the third argument actually is Every property read in JavaScript carries two objects, not one: the object you *start* from and the object where the property is eventually *found*. Normally you only notice this with getters, where `this` is the starting object even though the accessor lives on a prototype. The specification calls the starting object the receiver, and `[[Get]](key, receiver)` threads it down the whole prototype chain. A proxy's `get` trap exposes that plumbing directly: `get(target, property, receiver)`. The `target` is the object you wrapped. The `receiver` is the object the access began on. ## When receiver is not the proxy For a direct access they are the same object: ```js const p = new Proxy({}, { get(target, key, receiver) { return receiver; }, }); p.anything === p; // true ``` Put the proxy on a prototype chain and they separate: ```js const child = Object.create(p); child.anything === child; // true — the trap ran, receiver is child ``` The lookup starts at `child`, finds nothing there, walks to its prototype (the proxy), and performs `[[Get]]` on it *with the original receiver preserved*. So the trap sees `target` = the wrapped object, but `receiver` = `child`. `Reflect.get(obj, key, receiver)` and `Reflect.set(obj, key, value, receiver)` let you pass an explicit receiver too, which is the other way to observe the split. ## Why it matters for reads: accessors get the wrong `this` The naive forwarding handler looks harmless: ```js const target = { first: 'Ada', get greeting() { return 'Hi ' + this.first; }, }; const p = new Proxy(target, { get(t, key) { return t[key]; }, // receiver discarded }); const child = Object.create(p); child.first = 'Grace'; child.greeting; // 'Hi Ada' — wrong ``` `t[key]` performs a fresh `[[Get]]` whose receiver is `t`, so the `greeting` getter runs with `this === target` and reads the target's own `first`. Every property the getter touches also bypasses the proxy, so an instrumenting handler silently under-reports. Forwarding the receiver instead — `Reflect.get(t, key, receiver)` — makes the getter run with `this === child` and yields `'Hi Grace'`. ## Why it matters for writes: the property lands on the wrong object The `set` trap signature is `set(target, property, value, receiver)`, and here the consequence is structural rather than cosmetic. Ordinary assignment semantics say that when a write is satisfied by an inherited data property, the new property is created *on the receiver*, not on the prototype. So: ```js const store = {}; const p = new Proxy(store, { set(t, key, value) { t[key] = value; return true; }, // receiver discarded }); const a = Object.create(p); const b = Object.create(p); a.count = 1; b.count; // 1 — leaked through the shared prototype ``` Both children now share one slot on `store`, because the handler wrote to the target instead of to the object the assignment was aimed at. With `Reflect.set(t, key, value, receiver)` each child gets its own own-property and they stay independent. This is the bug the receiver argument exists to prevent, and it is exactly the kind of thing an interviewer means by "do you understand what the proxy is standing in for". ## The self-reference footgun One trap to watch: if you forward a *write* with the receiver and the receiver is the proxy itself — the common direct-access case — the write is re-dispatched through the proxy, and a handler that unconditionally forwards can recurse until the stack overflows. The usual defence is to forward reads and writes to the target while only passing the receiver on, rather than re-entering the proxy, and to be deliberate about which object each operation is meant to land on. ## Which traps carry a receiver Only `get` and `set` do — they are the operations the language defines as receiver-carrying. `has`, `deleteProperty`, `ownKeys`, `getOwnPropertyDescriptor` and `defineProperty` take no receiver, because those operations are defined on a single object and never thread a starting point down the chain. If you find yourself wanting a receiver in `has`, the model to correct is your mental model of the operation, not the trap signature. ## What to say in an interview Define it in one line — "the object the access started on, and the `this` for any accessor" — then produce the prototype-chain case, then name the two failures: getters reading the target's state instead of the real object's, and inherited writes piling up on a shared prototype. That progression shows you know the mechanism rather than the signature.

  • Which proxy traps take a receiver argument, and why only those?
    Only `get` (third argument) and `set` (fourth). Those are the two internal methods the specification defines as threading a receiver down the prototype chain, because both can be satisfied by an inherited property while still acting on the original object. `has`, `deleteProperty`, `ownKeys`, `getOwnPropertyDescriptor` and `defineProperty` are defined on one object only, so there is no starting point to carry.
  • How would you spot a discarded-receiver bug in a codebase that already ships such a proxy?
    Look for state that appears on the wrong object: two instances sharing a value they should not, or a getter returning stale data that matches the wrapped object rather than the caller. Then check whether the proxy is ever used as a prototype at all — if nothing inherits from it, the bug is latent, and it appears the day someone calls `Object.create` on it.
  • What causes infinite recursion in a `set` trap that forwards the receiver?
    If the receiver is the proxy itself, forwarding the write with that receiver re-dispatches the assignment through the same proxy, which calls the trap again. Forward to the target as the object being operated on and pass the receiver only as the receiver; if you must handle a self-receiver, detect it and write to the target directly.

saying these in an interview costs you the question

  • Says receiver is always the same as the proxy
  • Confuses receiver with the wrapped target object
  • Forwards with target[key] and calls it equivalent
  • Expects has or deleteProperty to receive a receiver too
  • Thinks an inherited assignment writes onto the prototype by default

context