cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Proxy

Learn how JavaScript Proxy objects intercept reads, writes, calls, construction, object inspection, validation, revocation, and the invariants engines still enforce.

By the end, you’ll be able to
  • 01
    Name the moving partsExplain target, handler, trap, forwarding, and invariants.
  • 02
    Choose the right trapConnect everyday operations to the 13 proxy traps.
  • 03
    Use proxies safelyBuild validation, observable, and revocable wrappers without breaking language rules.

Proxies in one sentence

A Proxy is an object that stands in front of another object or function, called the target. Code talks to the proxy. The proxy’s handler can intercept operations such as reading a property, writing a property, checking in, deleting, listing keys, calling a function, or using new.

One-sentence definition

new Proxy(target, handler) creates a wrapper whose handler methods, called traps, can customize how operations on the wrapper behave.

This lesson builds directly on Property flags & descriptors, Getters & setters, The prototype chain, and Private fields & methods. A proxy is broader than a getter or setter: it can watch an entire object, not just one property name.

Real-life analogyA proxy is a personal assistant

Imagine a busy boss who never answers the phone directly. Every message goes through an assistant. If the assistant has a rule for that kind of request, the rule runs. If not, the request goes straight to the boss.

In real life: The boss
In JavaScript: The target object or function
In real life: The assistant at the door
In JavaScript: The proxy object code talks to
In real life: A request to read, write, delete, call, or hire
In JavaScript: An operation such as get, set, delete, apply, or construct
In real life: The assistant’s rule book
In JavaScript: The handler object full of traps

Where the analogy stops: A real assistant can improvise. A proxy can only respond through the traps JavaScript defines, and the engine checks company policy: proxy invariants.

Proxies are a metaprogramming feature. They let code change how other code interacts with an object. That power is useful for validation, logging, reactive state, access control, and wrappers around APIs. It is also easy to overuse, so this lesson is careful about the limits.

Handlers and traps

INTERACTIVE

A proxy always has two pieces: a target and a handler. The handler is an ordinary object. Its method names are special: get catches property reads, set catches property writes, and so on. Each method is called a trap because the operation falls into it before it reaches the target.

Real-life analogyTraps are the assistant’s rule book

The assistant does not make up a new rule for every visitor. The rule book has one chapter per kind of request. JavaScript’s handler works the same way: one trap name per operation category.

In real life: Rule: screen all phone calls
In JavaScript: apply for function calls
In real life: Rule: check the visitor list
In JavaScript: has for the in operator
In real life: Rule: refuse bad paperwork
In JavaScript: set validation
In real life: No rule for a request
In JavaScript: Forward with normal target behavior

Where the analogy stops: The rule book has fixed chapter names. A trap named read does nothing because JavaScript only recognizes get.

Proxy compared with nearby tools
ToolInterceptsBest useImportant limit
Getter or setterOne named propertyComputed or validated fields such as user.fullNameCannot see in, delete, Object.keys, calls, or construction
ProxyMany operations on a whole targetLogging, validation, membranes, observable models, API wrappersMust obey proxy invariants and can break private fields or internal-slot methods
Descriptor flagsWhether a property is writable, enumerable, configurableLocking or hiding specific propertiesDoes not run custom code by itself

The safest trap usually forwards to the matching Reflect method. For example, Reflect.get(target, property, receiver) performs the normal property read and preserves important details such as getters and the receiver. The next lesson, Reflect, is the companion API for this forwarding pattern.

All 13 traps and what fires them

TRAP LOGGER

JavaScript defines exactly 13 proxy traps. Most targets are plain objects, so the first 11 cover object operations. The last two, apply and construct, work only when the target is callable or constructable.

The 13 Proxy traps
TrapOperation examplesWhat it must return
getproxy.name, optional chaining, method lookupThe value to read
setproxy.name = valueA boolean; strict mode throws if it is falsy
has"name" in proxyA boolean
deletePropertydelete proxy.nameA boolean
ownKeysObject.keys, Reflect.ownKeys, object spreadAn array-like list of string or symbol keys
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor, Object.keys filteringA descriptor or undefined
definePropertyObject.definePropertyA boolean
getPrototypeOfObject.getPrototypeOfAn object or null
setPrototypeOfObject.setPrototypeOfA boolean
isExtensibleObject.isExtensibleA boolean matching the target
preventExtensionsObject.preventExtensionsA boolean; if true the target must really be non-extensible
applyproxy(...)The call result; only for callable targets
constructnew proxy(...)An object; only for constructable targets

The logger below is the heart of the lesson. Press each operation and watch the trap sequence. Notice that high-level tools may call more than one trap: Object.keys(proxy) asks for ownKeys, then checks descriptors so it can return only enumerable string keys.

Trap logger: which trap fires?
The handler has all 13 trapsPop out in the code editor (opens in a new tab)JavaScript
const target = { name: "Ada", role: "admin" };const handler = {  get(target, property, receiver) {    return Reflect.get(target, property, receiver);  },  set(target, property, value, receiver) {    return Reflect.set(target, property, value, receiver);  },  has(target, property) {    return Reflect.has(target, property);  },  deleteProperty(target, property) {    return Reflect.deleteProperty(target, property);  },  ownKeys(target) {    return Reflect.ownKeys(target);  },  getOwnPropertyDescriptor(target, property) {    return Reflect.getOwnPropertyDescriptor(target, property);  },  defineProperty(target, property, descriptor) {    return Reflect.defineProperty(target, property, descriptor);  },  getPrototypeOf(target) {    return Reflect.getPrototypeOf(target);  },  setPrototypeOf(target, prototype) {    return Reflect.setPrototypeOf(target, prototype);  },  isExtensible(target) {    return Reflect.isExtensible(target);  },  preventExtensions(target) {    return Reflect.preventExtensions(target);  },  apply(target, thisArg, args) {    return Reflect.apply(target, thisArg, args);  },  construct(target, args, newTarget) {    return Reflect.construct(target, args, newTarget);  },};const proxy = new Proxy(target, handler);
Operation you just ranPop out in the code editor (opens in a new tab)JavaScript
proxy.name;
Trap log1 call
  1. get args: "name" returns: "Ada"
Operation result"Ada"
Target own data[["name","Ada"],["role","admin"]]

Available traps: get, set, has, deleteProperty, ownKeys, getOwnPropertyDescriptor, defineProperty, getPrototypeOf, setPrototypeOf, isExtensible, preventExtensions, apply, construct. If a trap is missing, the operation passes through to the target’s ordinary behavior.

Try it yourself

Read proxy.name fired get.

Each button recreates a fresh target, performs one real operation, and logs the traps in order.

A handler does not have to define every trap. Missing traps are transparent: JavaScript performs the target’s ordinary behavior. Professional proxy code often defines only one or two traps and forwards everything else with Reflect.

Which bucket does each operation belong in?
  • proxy.total
  • proxy.total = 3
  • "total" in proxy
  • delete proxy.total
  • Object.keys(proxy)
  • Object.defineProperty(proxy, "x", desc)
  • Object.getPrototypeOf(proxy)
  • Object.preventExtensions(proxy)
  • proxy(2, 3)
  • new proxy("Ada")
Try it yourself
0 of 10 correct

Sort each operation by the part of proxy behavior it mainly uses.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Validation proxies

STEP THROUGH

The set trap is a natural place to validate data before it enters an object. In strict mode, a successful set trap must return a truthy value. Returning false, or forgetting to return anything, means the assignment failed and strict code throws a TypeError.

Step through a set trap
Step 0 of 8Ready
Your turn: follow the blue line

Choose an age, then step through the recorded set trap. The replay uses the real validation function.

Running in
  1. script
Next: line 2
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
"use strict";const userProxy = new Proxy(user, {  set(target, property, value, receiver) {    if (property === "age") {      if (typeof value !== "number") {        throw new TypeError("age must be a number");      }      if (value < 0) {        throw new TypeError("age must be non-negative");      }    }    return Reflect.set(target, property, value, receiver);  },}); userProxy.age = 36;console.log(user.age);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the assignment on line 18

Changing the setting restarts the replay with a recording from a real run.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

This is similar to a setter from Getters & setters, but wider. One set trap can validate many property names, log every change, or block unknown fields. The trade-off is clarity: someone reading userProxy.age = 36 has to know a proxy stands in the middle.

Use Reflect.set unless you have a reason not to

Reflect.set gives the same boolean result the trap must return. It also handles inherited setters and receivers correctly. The next lesson focuses on Reflect because forwarding by hand is a common source of bugs.

Observable objects

INTERACTIVE

A proxy can watch writes and deletions, then notify another part of your program. This pattern is called an observable object. The small lab below wraps a cart, records changes, and re-renders a tiny summary each time.

Observable cart lab
Observable object wrapperPop out in the code editor (opens in a new tab)JavaScript
const cart = { apples: 1, pears: 2 };const changes = [];const observed = new Proxy(cart, {  set(target, property, value, receiver) {    const before = target[property];    const ok = Reflect.set(target, property, value, receiver);    changes.push({ type: "set", property, before, after: value });    render();    return ok;  },  deleteProperty(target, property) {    const before = target[property];    const ok = Reflect.deleteProperty(target, property);    changes.push({ type: "delete", property, before });    render();    return ok;  },});
Cart summary3 items
apples1
pears2

Change log

No changes yet. Try a button.

Try it yourself

The proxy records each set or delete, then the tiny cart view re-renders from the changed target.

This is a tiny teaching version of observable state; real frameworks add batching, dependency tracking, and many safeguards.

Real reactive systems are more sophisticated. They track which views depend on which properties, batch updates, avoid infinite loops, and integrate with scheduling. But the first idea is the same as the lab: intercept the operation, forward it, then notify interested code.

Revocable proxies

INTERACTIVE

Sometimes a wrapper should stop working: a plugin unloads, a permission expires, or a temporary API view must be closed. Proxy.revocable creates exactly that. It returns an object with proxy and revoke. After revoke(), every later operation on the proxy throws a TypeError.

Revocable proxy lab
A proxy you can turn offPop out in the code editor (opens in a new tab)JavaScript
const target = { secret: "draft" };const { proxy, revoke } = Proxy.revocable(target, {  get(target, property, receiver) {    return Reflect.get(target, property, receiver);  },}); console.log(proxy.secret);revoke();console.log(proxy.secret);
Resultready
Before revokenot run
After revokenot run
Try it yourself

Press the button to read through the proxy, call revoke(), then try the same read again.

Proxy.revocable returns an object with two properties: proxy and revoke. Revocation is permanent.
Real-life analogyRevocable proxy means firing the assistant

If the assistant is fired, the old phone number no longer reaches the boss. Existing references to the proxy become unusable, even though the target may still be reachable elsewhere.

In real life: The assistant works at the desk
In JavaScript: Use proxy normally
In real life: The company ends the contract
In JavaScript: Call revoke()
In real life: Future callers reach no one
In JavaScript: Any later proxy operation throws TypeError

Where the analogy stops: The boss, the target object, may still exist. Revocation only disables that proxy wrapper.

Invariants and limitations

REAL ERRORS

A proxy is powerful, but it is not allowed to break the language’s promises. These promises are called invariants. If a target has a non-configurable, non-writable data property, the get trap cannot pretend its value is different. If the target is non-extensible, traps cannot invent extra own keys. When a trap breaks an invariant, JavaScript throws a TypeError.

Real-life analogyInvariants are company policy

The assistant can screen requests, but cannot falsify locked company records. If the file says a fact is final, the assistant is not allowed to tell visitors something else.

In real life: The official file says the fact is locked
In JavaScript: A non-configurable, non-writable descriptor
In real life: The assistant claims a different fact
In JavaScript: A lying trap result
In real life: Policy overrides the assistant
In JavaScript: The engine throws TypeError

Where the analogy stops: Company policy is human judgment. Proxy invariants are precise specification rules enforced by the engine every time.

Invariants and limitations lab
Invariant: locked facts stay lockedPop out in the code editor (opens in a new tab)JavaScript
const target = {};Object.defineProperty(target, "answer", {  value: 42,  writable: false,  configurable: false,});const liar = new Proxy(target, {  get() {    return 43;  },});liar.answer;
ResultTypeError
error.nameTypeError
outputnone
Try it yourself

Lie about locked property throws TypeError. That is the real language rule protecting the target or built-in object.

The lab reports error.name instead of relying on engine-specific messages.

Lying about locked data

Invariant failurePop out in the code editor (opens in a new tab)JavaScript
const target = {};Object.defineProperty(target, "answer", {  value: 42,  writable: false,  configurable: false,});const liar = new Proxy(target, {  get() {    return 43;  },});liar.answer;

Private fields do not forward through wrappers

Private field receiverPop out in the code editor (opens in a new tab)JavaScript
class SecretBox {  #secret = "safe";  read() {    return this.#secret;  }}const box = new SecretBox();const proxy = new Proxy(box, {});proxy.read();

Internal slots need the real built-in object

Map method bindingPop out in the code editor (opens in a new tab)JavaScript
const map = new Map([["score", 10]]);const proxy = new Proxy(map, {});proxy.get("score"); const fixed = new Proxy(map, {  get(target, property, receiver) {    const value = Reflect.get(target, property, receiver);    return typeof value === "function" ? value.bind(target) : value;  },});fixed.get("score");

A proxy has its own identity

Proxy identityPop out in the code editor (opens in a new tab)JavaScript
const target = { id: 1 };const proxy = new Proxy(target, {});console.log(proxy === target);

Where you’ll use this

Proxies are usually library tools rather than everyday application tools. You will meet them in state libraries, API clients, validation layers, plugin systems, testing helpers, and security membranes. A membrane is a wrapper boundary: outside code receives proxies instead of direct objects, so every crossing can be logged, limited, or revoked.

  • Validation: reject invalid writes before they enter a shared model.
  • Observation: record every set and delete so a view can update.
  • Virtual objects: compute values lazily or represent data that lives somewhere else.
  • Access control: expose a safe subset of a larger object, sometimes with Proxy.revocable.
Prefer boring tools first

If one property needs computed behavior, use a getter or setter. If one property needs flags, use descriptors. Reach for Proxy when you truly need a whole-object boundary.

Common misconceptions

“A proxy is a copy of the target.”

No. It is a wrapper with a separate identity. Operations can still mutate the original target if the traps forward them.

“A trap can return anything it wants.”

Traps have contracts. set, defineProperty, deleteProperty, setPrototypeOf, isExtensible, and preventExtensions have boolean expectations and invariant checks.

“Object.keys only calls ownKeys.”

It also needs descriptors so it can filter to enumerable string keys. The trap logger shows both calls.

“Private fields and Map internals are just properties.”

They are not. Private fields and built-in internal slots require the right receiver. A transparent proxy can still break them.

“Revoking deletes the target.”

Revocation disables that proxy only. Other references to the target keep working.

Practice: proxies at the keyboard

5 EXERCISES
Exercise 1 · Warm-upPredict a get trap

Predict the two console lines.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const target = { name: "Ada" };
const proxy = new Proxy(target, {
  get(target, property, receiver) {
    console.log("get " + String(property));
    return Reflect.get(target, property, receiver);
  },
});
console.log(proxy.name);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticePredict a set trap

    Predict what this strict-mode assignment logs.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    "use strict";
    const target = { count: 0 };
    const proxy = new Proxy(target, {
      set(target, property, value, receiver) {
        console.log("set " + String(property) + "=" + value);
        return Reflect.set(target, property, value, receiver);
      },
    });
    proxy.count = 2;
    console.log(target.count);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFind the bug in a set trap

      The successful write path forgets something. What should it return?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      "use strict";
      const user = { age: 0 };
      const proxy = new Proxy(user, {
        set(target, property, value) {
          if (property === "age" && value < 0) return false;
          target[property] = value;
        },
      });
      proxy.age = 12;

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeRecord one observable change

        What change string does this observable object print?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const changes = [];
        const cart = { apples: 1 };
        const observed = new Proxy(cart, {
          set(target, property, value, receiver) {
            const before = target[property];
            const ok = Reflect.set(target, property, value, receiver);
            changes.push(String(property) + ":" + before + "->" + value);
            return ok;
          },
        });
        observed.apples = 3;
        console.log(changes[0]);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeUse and revoke

          What is the second logged line after the proxy is revoked?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const target = { secret: "draft" };
          const { proxy, revoke } = Proxy.revocable(target, {});
          console.log(proxy.secret);
          revoke();
          try {
            console.log(proxy.secret);
          } catch (error) {
            console.log(error.name);
          }

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz: check your understanding

            7 QUESTIONS

            Try the answer first, then read the explanations. Proxy details reward careful wording.

            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What is a Proxy in JavaScript?

              Choose an answer to see the explanation.

            2. Question 2 of 7Which trap handles a property read?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const proxy = new Proxy({ name: "Ada" }, {
                get(target, property, receiver) {
                  console.log("trap " + String(property));
                  return Reflect.get(target, property, receiver);
                },
              });
              console.log(proxy.name);

              Choose an answer to see the explanation.

            3. Question 3 of 7What does Object.keys usually trigger on a proxy?

              Choose an answer to see the explanation.

            4. Question 4 of 7What must a successful set trap return in strict mode?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              "use strict";
              const proxy = new Proxy({}, {
                set() {
                  return false;
                },
              });
              try {
                proxy.x = 1;
              } catch (error) {
                console.log(error.name);
              }

              Choose an answer to see the explanation.

            5. Question 5 of 7Why can a proxy not lie about a locked data property?

              Choose an answer to see the explanation.

            6. Question 6 of 7What does Proxy.revocable return?

              Choose an answer to see the explanation.

            7. Question 7 of 7Which limitation is real?

              Choose an answer to see the explanation.

            Key takeaways

            • A proxy is a wrapper around a target, controlled by a handler object.
            • Traps are handler methods named after operation categories; missing traps pass through.
            • There are 13 traps: 11 object traps plus apply and construct for function targets.
            • Use Reflect to forward ordinary behavior and return the right trap result.
            • Invariants are enforced with TypeError; proxies cannot lie about locked target facts.
            • Private fields, internal slots, and identity do not magically forward through proxies.

            Remember the one-liner.
            A Proxy is an intercepting wrapper: target plus handler, with traps for operations and invariants the traps cannot break.

            Up next: Reflect, the companion API for forwarding proxy operations cleanly and predictably.

            CompleteFrontend Clear concepts. Working examples.