cswork / libraryOpen practice workspace

Articles

JavaScript Proxy Interview Questions: Invariants, Receivers, and Reflect

Practice JavaScript Proxy interview questions with receiver traces, Reflect forwarding, invariant failures, private fields, and internal-slot boundaries.

A logging proxy works around a plain object, then fails when a getter reads a private field. A second proxy reports a successful assignment without changing anything. A third invents a value for a frozen property and throws. Proxy interview questions are less about remembering trap names than understanding which operation was intercepted and which contract still applies.

The short answer is that a JavaScript Proxy wraps selected internal operations, not every aspect of an object's identity. Reflect helps forward the default operation and its receiver. It does not waive invariants, transfer private brands, or give a wrapper the internal slots of a Map. Define the wrapper's supported object domain before calling it transparent.

Documented language behavior: the trap rules below are attributed to the linked MDN references. The diagnostic exercises and wrapper-design recommendations are original preparation material and inference, without candidate reports or employer frequency claims.

What operation does a trap intercept?

A Proxy associates a target with a handler whose traps customize supported object operations. A get trap handles property access; a set trap handles assignment; other traps govern property definition, enumeration, prototypes, or invocation. The target remains the object whose constraints limit what the wrapper may report.

MDN's Proxy reference explains the relationship between traps and object internal methods. Start a debugging answer by naming the operation. Reading proxy.value, testing 'value' in proxy, and calling Object.keys(proxy) are not one interchangeable event.

A get trap alone does not intercept all enumeration or property-descriptor queries. Similarly, a wrapper around an object does not rewrite every direct reference to its target elsewhere in the application. Owners with the raw target can still bypass the wrapper's logging or validation.

For this article's original logging exercise, the contract is deliberately narrow: preserve ordinary property access on ordinary objects while recording the operation. It does not promise a security boundary or support every built-in instance. That scope makes both the implementation and its tests falsifiable.

Why does receiver matter for getters?

The receiver is the value used as this when an accessor is invoked through the property operation. It can differ from the object on which the descriptor was found. MDN's Reflect.get documentation describes this explicit receiver argument.

Use this original fixture:

const target = {
  label: 'target',
  get display() { return this.label; }
};
const proxy = new Proxy(target, {
  get(t, key, receiver) {
    return Reflect.get(t, key, receiver);
  }
});
const child = Object.create(proxy);
child.label = 'child';
console.log(child.display); // child

The getter is found through the target, but the receiver is child. Replacing the forwarding line with t[key] calls the getter with target as this, yielding target's label instead. A wrapper can preserve the apparent property value in a simple test while changing inherited getter semantics.

This is why the first test should not be only proxy.label. Add an inherited accessor and a different receiver. The difference is observable without a framework, a reactive library, or a complex application. Once that baseline is understood, you can discuss how dependency tracking uses intercepted reads.

What does Reflect preserve, and where can recursion occur?

Reflect exposes operations corresponding to object internal behavior. Passing the target and original receiver usually preserves more of the default semantics than hand-written indexing. However, calling Reflect on the proxy itself can re-enter the same trap.

Inside a get trap, Reflect.get(proxy, key, receiver) is not a magic instruction to bypass the proxy. The proxy's get behavior is exactly what that operation invokes. Unless the code has a deliberate terminating policy, it can recurse until the stack fails.

The distinction is target versus wrapper, not Reflect versus ordinary syntax. Both mechanisms can enter traps depending on the object receiving the operation. Draw the call path explicitly: caller → proxy → handler → raw target operation. A cycle back to proxy deserves a separate explanation.

Logging itself can create unexpected extra reads if it formats or inspects the wrapped value. Keep diagnostic output small and avoid recursively serializing arbitrary objects in a trap. Otherwise the instrumentation changes the event sequence it is trying to explain.

Which invariants can a get trap violate?

A trap cannot invent arbitrary observations for every target property. MDN's get-trap reference lists invariants. A non-configurable, non-writable own data property must be reported consistently with its actual stored value.

This original counterexample deliberately breaks that rule:

const fixed = Object.defineProperty({}, 'id', {
  value: 7, writable: false, configurable: false
});
const liar = new Proxy(fixed, {
  get() { return 9; }
});
console.log(liar.id); // throws TypeError

The target descriptor, not the handler's preferred story, defines the boundary. Returning a useful-looking fabricated value does not make the wrapper valid. Read the descriptor before claiming a Proxy can simulate any object without constraints.

A non-configurable accessor with no getter has a related restriction: the reported read must respect that missing getter. Other traps have their own invariants. Hiding a key from ownKeys, for example, is not governed solely by the get trap's rules.

When an interviewer asks about a virtual property layer, explain which properties are configurable, whether the target is extensible, and how enumeration and descriptors remain consistent. A design that works only before Object.freeze may still be useful, but its lifecycle boundary must be intentional.

Why must a set trap return a Boolean?

A set trap reports whether the assignment operation succeeded. MDN's set-trap documentation explains Boolean conversion and strict-mode failure when the trap reports false. This return value is not the same as returning the assigned value.

An original validation wrapper can reject negative scores and otherwise return Reflect.set(target, key, value, receiver). Reflect.set's documented result lets the caller observe whether the underlying operation succeeded. If you instead return value, assigning zero can be reported as failure because zero is falsy.

An exception does not imply a rollback. In the original strict-mode fixture, the trap first writes zero into the target, then returns zero. The assignment throws because the reported result is falsy, but the stored score is already zero. A caller that catches the error cannot safely assume the old value survived.

Wrapper behaviorFinal state and caller observation
Reflect forwards the zero-valued writeScore becomes zero; assignment succeeds.
Trap stores zero, then returns zeroScore becomes zero; strict assignment throws.
Trap returns true without storingScore remains unchanged; assignment appears successful.

All three cases were asserted in the saved Node fixture. For validation, reject an invalid value before forwarding. For logging, preserve the underlying result instead of conflating the assigned value with an operation-success flag.

Returning true after silently discarding an assignment is another contract. It may intentionally simulate a no-op on a configurable target, but it is not transparent storage. Some target descriptors prohibit claiming success for an incompatible write. Invariants still apply even if the handler returns a truthy value.

Forwarding with the original receiver matters for setters too. An inherited setter may need to update a child rather than the raw target. A direct target[key] = value can bypass that distinction. Test the resulting property location as well as the reported Boolean.

Also watch instrumentation that installs both set and defineProperty traps. A forwarded assignment can involve property definition on the receiver, producing more than one logged event. Decide whether your log describes low-level operations or one user-level assignment; do not count them interchangeably.

Why do private fields fail through an empty proxy?

Private fields use a class-specific brand check. An ordinary Proxy around a class instance is not automatically an instance carrying the target's private storage for those checks. Passing the proxy as this to a private-field getter or method can therefore throw.

Use this original fixture:

class Vault {
  #code = 7;
  get code() { return this.#code; }
  read() { return this.#code; }
}
const vault = new Vault();
const wrapped = new Proxy(vault, {});
// wrapped.code and wrapped.read() throw TypeError

There are two relevant operations: invoking the getter during a property read and calling the returned method with the proxy as receiver. Fixing only one does not fix the other. This is a useful follow-up after the ordinary getter fixture, because the apparently correct forwarding receiver is exactly what fails the brand requirement.

Binding selected methods to the raw target or using the target as an accessor receiver can support a deliberate adapter. It changes the semantics from ordinary receiver forwarding, however. Inherited overrides, fluent methods returning this, callback identity, and method extraction deserve tests before you call the adapter transparent.

Do not recommend binding every function without qualification. Creating a new bound function on each read changes identity, which can interfere with listener removal or caching. A method cache might address identity, but then you own invalidation when the target method changes. A small explicit facade can be clearer than a universal Proxy repair.

Why does Map expose another identity boundary?

Built-in objects can depend on internal slots rather than ordinary visible properties. A proxy around a Map does not acquire the target Map's internal storage. Accessing size or calling methods with the wrapper as the built-in receiver can fail.

This is related to private fields but not the same mechanism. The shared lesson is that property interception does not transfer every hidden aspect of the target. Explain the receiver required by each operation instead of diagnosing every TypeError as a broken import or missing method.

A Map-specific wrapper can choose to call selected methods on the raw Map and expose a carefully supported API. It must decide what set returns, whether iterator methods expose raw entries, and whether consumers can escape to the underlying object. That is an adapter contract, not an invisible membrane by default.

For a reactive state library, collection handling may require purpose-built instrumentation. Do not infer its behavior from one plain-object get trap. Library-specific subscriptions and dependency tracking are additional mechanisms that need their own source and tests.

What should the test matrix contain?

The original wrapper exercise needs direct data reads, missing keys, inherited getters with a child receiver, writable and fixed descriptors, successful and rejected writes, zero-valued assignments, private-field getters and methods, and a Map operation. Each case targets a different boundary rather than repeating the same happy path.

Run in strict mode when checking failed assignments, and record the runtime. Assert both operation result and final state. A trap can return success while leaving storage unchanged, so a Boolean assertion alone does not establish the intended contract.

Keep negative tests that expect TypeError. They document the unsupported object domain instead of hiding it behind broad exception handling. If a Proxy is only intended for plain records, reject unsupported inputs deliberately and describe the supported subset.

The receiver, frozen-property, zero-valued assignment, private-brand, and Map-slot fixtures were executed locally on Node 26.9.0. Those checks establish the small language examples, not reactive-library compatibility or a cross-browser support matrix.

Sources and Further Reading

Research note: Official technical references checked October 1, 2026. Examples are original language-level fixtures, not production reactive-library or cross-browser compatibility measurements.