Proxy & Reflect
Reviewed & published by Brayan K
A Proxy wraps an object with handler traps that intercept operations like reading, writing, and checking properties, while Reflect provides matching methods to perform the default behavior — together they let you customize how objects respond to ordinary code.
Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
This is metaprogramming: the same machinery behind reactive frameworks, validation layers, and tracing tools.
What You'll Learn in This Lesson
- Wrap objects with new Proxy()
- Intercept reads with the get trap
- Validate writes with the set trap
- Customize 'in' with the has trap
- Forward operations with Reflect
- Build defaults & negative indices
📚 Prerequisites: You should be comfortable with objects and getters and setters. Proxies generalize that idea to every operation, not just one named property.
🛰️ Real-World Analogy: A Proxy is like a receptionist in front of an office:
- • Every visitor (operation) goes through the desk instead of straight to the office (target)
- • The receptionist can log the visit, check ID (validate), or answer simple questions (defaults)
- • Reflect is the receptionist actually walking the request to the office the normal way
1️⃣ The get Trap: Defaults & Logging
new Proxy(target, handler) returns an object that behaves like target until one of the handler's traps fires. The get(target, key) trap runs on every property read, so you can log access or return a default for missing keys. Inside the trap, call Reflect.get to produce the normal result instead of touching target directly.
Use Reflect to forward: Reflect.get(target, key) does exactly what a plain read would, including respecting getters — safer than target[key] in edge cases.
const settings = new Proxy({ theme: "dark" }, {
get(target, key) {
if (key in target) return Reflect.get(target, key);
return "default";
},
});
console.log(settings.theme); // ✅ Expected output: dark
console.log(settings.size); // ✅ Expected output: default
// Logging reads
const logged = new Proxy({ x: 10 }, {
get(target, key) {
console.log("read " + String(key));
return Reflect.get(target, key);
},
});
console.log(logged.x);
// ✅ Expected output:
// read x
// 102️⃣ The set Trap: Validation
The set(target, key, value) trap fires on every assignment. Return true to accept the write (after storing it with Reflect.set) or throw to reject invalid data. Unlike a single setter, one trap guards all properties, so you can enforce a rule like "age must be a positive number" across the whole object.
Always return from set: the trap must return a truthy value (use Reflect.set's result). Returning false in strict mode throws a TypeError.
const user = new Proxy({}, {
set(target, key, value) {
if (key === "age" && (typeof value !== "number" || value < 0)) {
throw new TypeError("age must be a non-negative number");
}
return Reflect.set(target, key, value);
},
});
user.name = "Ada";
user.age = 36;
console.log(user.name, user.age); // ✅ Expected output: Ada 36
try {
user.age = -5;
} catch (e) {
console.log(e.message); // ✅ Expected output: age must be a non-negative number
}
console.log(user.age); // ✅ Expected output: 363️⃣ The has Trap & a Real Trick: Negative Indices
The has(target, key) trap intercepts the in operator — handy for hiding "private" keys or faking membership. Combining get and set traps over an array lets you implement Python-style negative indexing, where arr[-1] means the last element. It's a classic demonstration of how proxies extend built-in behavior.
Reactivity in a nutshell: frameworks like Vue use exactly this pattern — a get trap to track which data was read and a set trap to re-run anything that depended on it.
// Hide keys starting with _ from the 'in' operator
const obj = new Proxy({ name: "Ada", _secret: 1 }, {
has(target, key) {
if (String(key).startsWith("_")) return false;
return Reflect.has(target, key);
},
});
console.log("name" in obj); // ✅ Expected output: true
console.log("_secret" in obj); // ✅ Expected output: false
// Negative array indexing
function negArray(arr) {
return new Proxy(arr, {
get(t, prop) {
const i = Number(prop);
if (Number.isInteger(i) && i < 0) return Reflect.get(t, String(t.length + i));
return Reflect.get(t, prop);
},
});
}
const nums = negArray([10, 20, 30]);
console.log(nums[-1]); // ✅ Expected output: 30
console.log(nums[0]); // ✅ Expected output: 10🎯 Your Turn — Default Values
Complete the get trap so missing keys return 0.
const counts = new Proxy({ apples: 3 }, {
get(target, key) {
// 👉 replace ___ : if the key exists, forward it; otherwise return 0
return key in target ? Reflect.___(target, key) : ___;
},
});
console.log(counts.apples); // ✅ Expected output: 3
console.log(counts.bananas); // ✅ Expected output: 0🧠 Quick Recall
Predict the output before revealing the answer.
42 — the get trap ignores the key and always returns 42.
true — Reflect.has is the function form of the in operator.
9 — Reflect.set performs the assignment and returns true.
Common Errors & Fixes
- 1️⃣ Forgetting to return from the set trap
- 2️⃣ Reading target[key] inside a get trap
- 3️⃣ Expecting keys to be strings only
- 4️⃣ Using the target after proxying and expecting traps
🎯 Mini Challenge — Read-Only View
Wrap an object so reads work but every write is silently blocked.
function readOnly(target) {
return new Proxy(target, {
get(t, k) { return Reflect.get(t, k); },
set() { return true; }, // accept but ignore the write
});
}
const config = readOnly({ url: "/api", retries: 3 });
console.log(config.url); // ✅ Expected output: /api
config.retries = 99; // ignored
console.log(config.retries); // ✅ Expected output: 3Quick Reference — Common Traps
| Trap | Intercepts |
|---|---|
| get | reading a property |
| set | assigning a property |
| has | the in operator |
| deleteProperty | delete obj.x |
| Reflect.* | perform the default action |
📚 Keep Learning
- MDN: Proxy reference
- MDN: Reflect reference
- MDN: Proxy handler.get()
🎉 Lesson Complete — Proxy & Reflect!
- ✅ new Proxy(target, handler) intercepts operations via traps
- ✅ get for reads/defaults, set for validation, has for in
- ✅ Call Reflect.* inside a trap to perform the default behavior
- ✅ Real uses: reactivity, validation, logging, negative indices, read-only views
Practice quiz
What does new Proxy(target, handler) return?
- A frozen copy of target
- A deep clone with no methods
- An object that behaves like target until a handler trap fires
- The handler object itself
Answer: An object that behaves like target until a handler trap fires. A Proxy wraps the target and behaves like it until one of the handler's traps intercepts an operation.
Which trap intercepts reading a property?
- get
- set
- has
- deleteProperty
Answer: get. The get(target, key) trap runs on every property read.
For a get trap returning Reflect.get when the key exists and "default" otherwise, what is settings.size if size is missing?
- undefined
- null
- An error
- "default"
Answer: "default". Missing keys fall through to the default, so settings.size is "default".
What is the role of the set trap?
- It reads properties
- It fires on assignment and can validate the value
- It deletes properties
- It intercepts the in operator
Answer: It fires on assignment and can validate the value. set(target, key, value) fires on every assignment, letting you validate or reject the write.
Why should a set trap return Reflect.set(...)?
- It must return a truthy value or strict mode throws
- To delete the key
- To skip the assignment
- To log the read
Answer: It must return a truthy value or strict mode throws. The set trap must return truthy; Reflect.set performs the write and returns true.
Which trap intercepts the in operator?
- get
- set
- has
- ownKeys
Answer: has. The has(target, key) trap intercepts the in operator.
Using the negative-index proxy over [10, 20, 30], what does nums[-1] return?
- 10
- 30
- undefined
- -1
Answer: 30. The get trap maps -1 to length + (-1), returning the last element, 30.
What is Reflect used for inside a trap?
- To freeze the object
- To create a new Proxy
- To throw errors automatically
- To perform the default behavior of the operation
Answer: To perform the default behavior of the operation. Reflect methods (Reflect.get, Reflect.set, etc.) perform the default operation correctly.
When checking a key in a has trap, why use String(key).startsWith("_") instead of key.startsWith("_")?
- It is faster
- Symbol keys have no startsWith method
- Strings cannot use startsWith
- key is always a number
Answer: Symbol keys have no startsWith method. Keys can be symbols, which lack startsWith; coerce with String() first.
In a read-only proxy whose set trap returns true without writing, what happens to config.retries = 99?
- It updates to 99
- It throws a TypeError
- It is silently ignored
- It deletes retries
Answer: It is silently ignored. The set trap accepts the call but never stores it, so the write is silently ignored.
Continue this course
- Previous: Getters, Setters & Property Descriptors
- Next: BigInt & Number Precision — Work with integers beyond MAX_SAFE_INTEGER and avoid precision loss
- Quick reference: JavaScript cheat sheet
Frequently asked questions
What is a Proxy in JavaScript?
A Proxy wraps a target object with a handler whose 'trap' methods intercept fundamental operations — reading a property (get), assigning one (set), checking existence (has), deleting, and more. The proxy looks and behaves like the original object, but you get to run code on every interaction.
What is Reflect and why pair it with Proxy?
Reflect is a built-in object whose methods mirror the proxy traps: Reflect.get, Reflect.set, Reflect.has, and so on. Inside a trap you call the matching Reflect method to perform the default behavior correctly, then add your own logic around it. It is the clean way to 'forward' an operation.
What are some real uses for a Proxy?
Validation (reject bad assignments), default values (return a fallback for missing keys), logging and debugging, reactivity systems like Vue's, read-only views, and clever tricks such as supporting negative array indices. Anywhere you want to customize how an object responds, a Proxy fits.
Does a Proxy slow things down?
Every trapped operation runs your handler code, so a Proxy is slower than direct property access. For most app logic the cost is negligible, but avoid wrapping objects that are read in extremely hot loops where micro-performance matters.