Proxy
Learn how JavaScript Proxy objects intercept reads, writes, calls, construction, object inspection, validation, revocation, and the invariants engines still enforce.
- 01Name the moving partsExplain target, handler, trap, forwarding, and invariants.
- 02Choose the right trapConnect everyday operations to the 13 proxy traps.
- 03Use 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.
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.
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, orconstruct - 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
INTERACTIVEA 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.
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:
applyfor function calls - In real life: Rule: check the visitor list
- In JavaScript:
hasfor theinoperator - In real life: Rule: refuse bad paperwork
- In JavaScript:
setvalidation - 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.
| Tool | Intercepts | Best use | Important limit |
|---|---|---|---|
| Getter or setter | One named property | Computed or validated fields such as user.fullName | Cannot see in, delete, Object.keys, calls, or construction |
| Proxy | Many operations on a whole target | Logging, validation, membranes, observable models, API wrappers | Must obey proxy invariants and can break private fields or internal-slot methods |
| Descriptor flags | Whether a property is writable, enumerable, configurable | Locking or hiding specific properties | Does 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 LOGGERJavaScript 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.
| Trap | Operation examples | What it must return |
|---|---|---|
get | proxy.name, optional chaining, method lookup | The value to read |
set | proxy.name = value | A boolean; strict mode throws if it is falsy |
has | "name" in proxy | A boolean |
deleteProperty | delete proxy.name | A boolean |
ownKeys | Object.keys, Reflect.ownKeys, object spread | An array-like list of string or symbol keys |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor, Object.keys filtering | A descriptor or undefined |
defineProperty | Object.defineProperty | A boolean |
getPrototypeOf | Object.getPrototypeOf | An object or null |
setPrototypeOf | Object.setPrototypeOf | A boolean |
isExtensible | Object.isExtensible | A boolean matching the target |
preventExtensions | Object.preventExtensions | A boolean; if true the target must really be non-extensible |
apply | proxy(...) | The call result; only for callable targets |
construct | new 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.
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);proxy.name;- get args: "name" returns: "Ada"
"Ada"[["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.
Read proxy.name fired get.
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.
proxy.totalproxy.total = 3"total" in proxydelete proxy.totalObject.keys(proxy)Object.defineProperty(proxy, "x", desc)Object.getPrototypeOf(proxy)Object.preventExtensions(proxy)proxy(2, 3)new proxy("Ada")
Sort each operation by the part of proxy behavior it mainly uses.
Validation proxies
STEP THROUGHThe 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.
Choose an age, then step through the recorded set trap. The replay uses the real validation function.
script
"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);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.
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
INTERACTIVEA 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.
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; },});12Change log
No changes yet. Try a button.
The proxy records each set or delete, then the tiny cart view re-renders from the changed target.
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
INTERACTIVESometimes 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.
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);not runnot runPress the button to read through the proxy, call revoke(), then try the same read again.
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
proxynormally - 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 ERRORSA 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.
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.
const target = {};Object.defineProperty(target, "answer", { value: 42, writable: false, configurable: false,});const liar = new Proxy(target, { get() { return 43; },});liar.answer;TypeErrornoneLie about locked property throws TypeError. That is the real language rule protecting the target or built-in object.
Lying about locked data
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
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
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
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.
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 EXERCISESPredict the two console lines.
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);The read logs get name, then returns the target’s value, so the outer log prints Ada.
Predict what this strict-mode assignment logs.
"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);The set trap prints set count=2, forwards the write, and target.count becomes 2.
The successful write path forgets something. What should it return?
"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;return Reflect.set(target, property, value, receiver);Forwarding with Reflect.set performs the write and returns the boolean the trap contract expects.
What change string does this observable object print?
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]);Before the write, apples is 1. The proxy sets it to 3 and records apples:1->3.
What is the second logged line after the proxy is revoked?
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);
}The first line is draft. The second attempted read throws, and the catch block logs TypeError.
Quiz: check your understanding
7 QUESTIONSTry the answer first, then read the explanations. Proxy details reward careful wording.
Question 1 of 7What is a Proxy in JavaScript?
Choose an answer to see the explanation.
Question 2 of 7Which trap handles a property read?
Read the code, then predictconst 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.
Question 3 of 7What does Object.keys usually trigger on a proxy?
Choose an answer to see the explanation.
Question 4 of 7What must a successful set trap return in strict mode?
Read the code, then predict"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.
Question 5 of 7Why can a proxy not lie about a locked data property?
Choose an answer to see the explanation.
Question 6 of 7What does Proxy.revocable return?
Choose an answer to see the explanation.
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
applyandconstructfor function targets. - Use
Reflectto 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.