Proxies & Reflect Get Certified
Proxies and Reflect are JavaScript’s metaprogramming tools. A Proxy lets you intercept and customize fundamental operations on an object (reading, writing, deleting, etc.). Reflect provides the matching default behaviors so you can forward operations cleanly from inside a trap.
What Is a Proxy?
A Proxy wraps a target object with a handler. The handler contains “traps” — functions that run whenever a specific operation is performed on the proxy.
What this code does: Creates a basic Proxy that
logs every property access through the get trap.
const person = { name: "Michael", age: 32 };
const loggedPerson = new Proxy(person, {
get(target, property) {
console.log("Accessed property:", property);
return target[property];
}
});
console.log(loggedPerson.name);
console.log(loggedPerson.age);
const person = { name: "Michael", age: 32 }const loggedPerson = new Proxy(person, { — wraps the target; all access goes through the handler.get(target, property) { — runs on every property read.console.log("Accessed property:", property)return target[property] — without this, reads would be undefined.} }console.log(loggedPerson.name); console.log(loggedPerson.age); — each access logs then returns the value.The get Trap: Intercepting Property Reads
The get trap runs whenever a property is read. You can
return a custom value, provide defaults, or hide internal data.
What this code does: Returns a default message
when a missing property is accessed, instead of
undefined.
const settings = { theme: "dark" };
const safeSettings = new Proxy(settings, {
get(target, property) {
if (property in target) {
return target[property];
}
return `(no value for "${property}")`;
}
});
console.log(safeSettings.theme);
console.log(safeSettings.language);
const settings = { theme: "dark" }const safeSettings = new Proxy(settings, {get(target, property) {if (property in target) { return target[property]; } return \`(no value for "\${property}")\`; — missing properties no longer yield undefined.} }console.log(safeSettings.theme) — “dark”console.log(safeSettings.language) — friendly placeholder instead of undefined.The set Trap: Intercepting Property Writes
The set trap runs on every assignment. Returning
true signals that the assignment succeeded; returning
false (in strict mode) causes a TypeError.
What this code does: Intercepts property writes
with a set trap to validate that health cannot drop
below zero.
const player = { health: 100 };
const guardedPlayer = new Proxy(player, {
set(target, property, value) {
if (property === "health" && (typeof value !== "number" || value < 0)) {
throw new TypeError("Health must be a non-negative number.");
}
target[property] = value;
return true;
}
});
guardedPlayer.health = 75;
console.log(guardedPlayer.health);
try {
guardedPlayer.health = -10;
} catch (err) {
console.log("Error:", err.message);
}
const player = { health: 100 }const guardedPlayer = new Proxy(player, {set(target, property, value) { — runs on every assignment.if (property === "health" && (typeof value !== "number" || value < 0)) { throw new TypeError(...); }target[property] = valuereturn true — set traps should return true when the write succeeds (required in strict mode).} }guardedPlayer.health = 75; console.log(guardedPlayer.health);try { guardedPlayer.health = -10; } catch (err) { console.log("Error:", err.message); }The has Trap: Customizing the in Operator
The has trap runs whenever the in
operator is used to check a property, letting you hide certain
properties from that check.
What this code does: Hides internal properties
(those starting with _) from the
in operator.
const userRecord = { username: "michael_dev", _internalId: "u-8842" };
const publicUserRecord = new Proxy(userRecord, {
has(target, property) {
if (property.startsWith("_")) {
return false;
}
return property in target;
}
});
console.log("username" in publicUserRecord);
console.log("_internalId" in publicUserRecord);
const userRecord = { username: "michael_dev", _internalId: "u-8842" }const publicUserRecord = new Proxy(userRecord, {has(target, property) { — intercepts the in operator.if (property.startsWith("_")) { return false; } return property in target;} }console.log("username" in publicUserRecord) — trueconsole.log("_internalId" in publicUserRecord) — false, even though it exists on the target.The deleteProperty Trap
The deleteProperty trap runs whenever the
delete operator is used, letting you block deletion
of specific properties.
What this code does: Protects the
apiUrl property so it cannot be deleted.
const config = { apiUrl: "https://api.example.com", version: "1.0" };
const protectedConfig = new Proxy(config, {
deleteProperty(target, property) {
if (property === "apiUrl") {
console.log("Cannot delete apiUrl — it's required.");
return false;
}
delete target[property];
return true;
}
});
delete protectedConfig.apiUrl;
delete protectedConfig.version;
console.log(protectedConfig);
const config = { apiUrl: "...", version: "1.0" }const protectedConfig = new Proxy(config, {deleteProperty(target, property) { — intercepts the delete operator.if (property === "apiUrl") { console.log(...); return false; } — returning false means the delete failed.delete target[property]; return true;} }delete protectedConfig.apiUrldelete protectedConfig.versionconsole.log(protectedConfig) — apiUrl remains, version is gone.What Is Reflect?
The Reflect object provides methods that mirror the
default behavior a Proxy trap would otherwise have to reimplement
manually — Reflect.get(), Reflect.set(),
Reflect.has(), and more.
What this code does: Demonstrates
Reflect.get, Reflect.set,
Reflect.has, and Reflect.ownKeys.
const book = { title: "The Long Way Home", pages: 288 };
console.log(Reflect.get(book, "title"));
Reflect.set(book, "pages", 300);
console.log(book.pages);
console.log(Reflect.has(book, "title"));
console.log(Reflect.ownKeys(book));
const book = { title: "...", pages: 288 }Reflect.get(book, "title") — same as book.title, but as a function call useful inside traps.Reflect.set(book, "pages", 300) — returns true/false for success.console.log(book.pages)Reflect.has(book, "title") — same as "title" in book.Reflect.ownKeys(book) — all own keys (strings and symbols).
On its own, Reflect doesn’t do anything you couldn’t
already do with regular syntax — its real value shows up when
paired with Proxy traps.
Using Reflect Inside Proxy Traps
Inside a trap, using the matching Reflect method to
perform the default behavior is safer and more consistent than
manually reimplementing it, especially for edge cases like
inherited properties.
What this code does: Combines a Proxy with Reflect so the trap logs the operation and then forwards it to the default behavior.
const inventory = { apples: 12, bananas: 6 };
const loggedInventory = new Proxy(inventory, {
get(target, property, receiver) {
console.log("Getting:", property);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log("Setting:", property, "to", value);
return Reflect.set(target, property, value, receiver);
}
});
loggedInventory.apples;
loggedInventory.bananas = 10;
const inventory = { apples: 12, bananas: 6 }const loggedInventory = new Proxy(inventory, {get(target, property, receiver) { console.log("Getting:", property); return Reflect.get(target, property, receiver); } — receiver preserves correct this for getters.set(target, property, value, receiver) { console.log("Setting:", property, "to", value); return Reflect.set(...); } — preferred over manual target[property] = value.} }loggedInventory.applesloggedInventory.bananas = 10Revocable Proxies
Proxy.revocable() creates a proxy that can be
permanently disabled later, which is useful for revoking access
to an object once it’s no longer needed.
What this code does: Creates a revocable proxy
and shows that after revoke() any access throws a
TypeError.
const sessionData = { token: "abc123" };
const { proxy, revoke } = Proxy.revocable(sessionData, {});
console.log(proxy.token);
revoke();
try {
console.log(proxy.token);
} catch (err) {
console.log("Error:", err.message);
}
const sessionData = { token: "abc123" }const { proxy, revoke } = Proxy.revocable(sessionData, {}) — returns both the proxy and a revoke function.console.log(proxy.token)revoke() — permanently disables the proxy.try { console.log(proxy.token); } catch (err) { console.log("Error:", err.message); }
After calling revoke(), every operation on
proxy throws a TypeError, even simply
reading a property.
Common Proxy Traps
| Trap | Triggered By | Matching Reflect Method |
|---|---|---|
| get | Reading a property | Reflect.get() |
| set | Assigning a property | Reflect.set() |
| has | The in operator | Reflect.has() |
| deleteProperty | The delete operator | Reflect.deleteProperty() |
| ownKeys | Object.keys() and similar | Reflect.ownKeys() |
Practical Example
Let’s combine a few traps to build a simple validated form object where each field is checked against a schema.
What this code does: Builds a validated form
object where each field is checked against a schema inside the
set trap.
function createValidatedForm(schema) {
const data = {};
return new Proxy(data, {
set(target, property, value) {
const validator = schema[property];
if (validator && !validator(value)) {
console.log("Rejected invalid value for", property + ":", value);
return true;
}
target[property] = value;
return true;
}
});
}
const signupForm = createValidatedForm({
age: value => typeof value === "number" && value >= 18,
email: value => typeof value === "string" && value.includes("@")
});
signupForm.age = 25;
signupForm.age = 12;
signupForm.email = "not-an-email";
console.log(signupForm);
function createValidatedForm(schema)const data = {}return new Proxy(data, {set(target, property, value) {const validator = schema[property]if (validator && !validator(value)) { console.log("Rejected..."); return true; } — return true so the assignment “succeeds” without storing bad data (or throw if you prefer).target[property] = value; return true;} }const signupForm = createValidatedForm({ age: ..., email: ... })signupForm.age = 25signupForm.age = 12signupForm.email = "not-an-email"console.log(signupForm) — only valid fields stored.Common Mistakes With Proxies and Reflect
- Forgetting to return a value from the
settrap, which can cause silent or unexpected failures in strict mode. - Manually reimplementing default behavior inside a trap instead of delegating to the matching
Reflectmethod. - Modifying the original target object directly outside the proxy, expecting the proxy’s traps to somehow still apply.
- Using a proxy after it has been revoked, and being surprised by the resulting
TypeError. - Overusing proxies for simple cases where a plain function or getter/setter would be clearer and perform better.
Best Practices for Proxies and Reflect
- Use the matching
Reflectmethod inside a trap whenever you want the default behavior. - Keep trap logic focused on one concern — validation, logging, or defaults.
- Reach for a Proxy when you genuinely need to intercept operations dynamically; for known, fixed properties, getters and setters are often simpler.
- Use
Proxy.revocable()when you need to be able to permanently cut off access to an object later. - Document what a Proxy customizes, since its behavior isn’t visible just by looking at the target object.
Quick Summary
- A Proxy wraps a target object with a handler containing traps that intercept operations like reading, writing, and deleting properties.
- Common traps include
get,set,has, anddeleteProperty. Reflectprovides methods matching the default behavior of these operations, meant to be used inside traps.Proxy.revocable()creates a proxy that can later be permanently disabled.- Proxies are useful for validation, logging, default values, and controlling access to an object’s properties.
Try It Yourself
Modify the code below. Try changing the validation rule, or adding a new field with its own validator.
What this code does: A practice example that guards a product price so negative values are rejected while valid prices are stored normally.
const product = { price: 0 };
const guardedProduct = new Proxy(product, {
set(target, property, value) {
if (property === "price" && value < 0) {
console.log("Price cannot be negative.");
return true;
}
target[property] = value;
return true;
}
});
guardedProduct.price = 49.99;
console.log(guardedProduct.price);
guardedProduct.price = -5;
console.log(guardedProduct.price);
const product = { price: 0 }const guardedProduct = new Proxy(product, {set(target, property, value) {if (property === "price" && value < 0) { console.log("Price cannot be negative."); return true; }target[property] = value; return true;} }guardedProduct.price = 49.99console.log(guardedProduct.price)guardedProduct.price = -5console.log(guardedProduct.price) — still 49.99. Try changing the rule or adding another field.
Tip:
A helpful mental model: a Proxy is a customizable gatekeeper in
front of an object, and Reflect is the gatekeeper’s
default rulebook — you only need to override the specific rules
you actually want to change.
Understanding Proxies and Reflect is useful before moving on to modules, design patterns, and more advanced metaprogramming concepts in JavaScript.
🧪 Test Your JavaScript Code
Edit the code on the left and click "Run Code" to see the console output on the right.
Click "Run Code" to see the output here.
Code Explanation
The complete example stays visible. Use Previous and Next to move through its logical blocks.
Select Next to begin exploring the code.