Proxy & Reflect in JavaScript
A Proxy is a "middleman" that sits between you and an object. When someone tries to read, write, or delete a property on the object, the Proxy can intercept that operation and decide what to do.
Think of it like a receptionist at a front desk. You do not talk to the boss directly — you go through the receptionist, who can forward your request, say "he's busy", or even change the message before it reaches its destination.
JavaScript introduced Proxy in ES6 (2015). Before that, there was no built-in way to intercept property access on an object.
Here is what happens without a Proxy — direct access with no control:
// Without Proxy — direct access, no control
const user = { name: "Alice", age: 25 };
console.log(user.name); // "Alice"
user.age = -5; // No error! Bad data accepted silently.
console.log(user.age); // -5
Proxy lets you add a layer of control without changing the original object.
The syntax for creating a Proxy is:
const proxy = new Proxy(target, handler)
- target — the original object you want to wrap
- handler — an object with "trap" functions that intercept operations
If the handler is empty {}, the proxy just passes everything through — no changes at all.
const user = { name: "Alice", age: 25 };
// An empty handler — everything passes through unchanged
const proxy = new Proxy(user, {});
console.log(proxy.name); // "Alice" — same as user.name
proxy.age = 30;
console.log(user.age); // 30 — the original object is updated
Notice that reading or writing through the proxy actually affects the original target object. They are linked — the proxy does not make a copy.
Traps are the methods inside the handler object. Each trap intercepts a specific operation on the target.
The most common traps are:
- get — intercepts property reading
- set — intercepts property writing
- has — intercepts the in operator
- deleteProperty — intercepts delete
- apply — intercepts function calls (when target is a function)
Here is the get trap in action — it fires whenever a property is read:
const user = { name: "Alice", age: 25 };
const proxy = new Proxy(user, {
get(target, property) {
console.log(`Someone read: ${property}`);
return target[property]; // return the real value
}
});
console.log(proxy.name);
// Logs: "Someone read: name"
// Then: "Alice"
console.log(proxy.email);
// Logs: "Someone read: email"
// Then: undefined (property doesn't exist)
And here is the set trap with validation — it fires whenever a property is assigned a value:
const user = { name: "Alice", age: 25 };
const proxy = new Proxy(user, {
set(target, property, value) {
if (property === "age" && typeof value !== "number") {
throw new TypeError("Age must be a number!");
}
if (property === "age" && value < 0) {
throw new RangeError("Age cannot be negative!");
}
target[property] = value; // apply the change
return true; // must return true to confirm the set
}
});
proxy.age = 30; // works fine
console.log(user.age); // 30
// proxy.age = "thirty"; // throws TypeError
// proxy.age = -1; // throws RangeError
If a trap is not defined in the handler, the operation works normally on the target — no interception takes place.
Validation — intercept set to check data before it is stored (shown in the previous section).
Default values — intercept get to return a fallback when a property is undefined:
const settings = { theme: "dark" };
const proxy = new Proxy(settings, {
get(target, property) {
// If property doesn't exist, return a default instead of undefined
return property in target ? target[property] : "default";
}
});
console.log(proxy.theme); // "dark"
console.log(proxy.language); // "default" (not undefined)
console.log(proxy.fontSize); // "default"
Logging / observing — intercept any operation to log what is happening (useful for debugging). This is especially powerful in development when you want to track every read and write on an object.
Read-only objects — intercept set to prevent all changes:
const config = { apiUrl: "https://api.example.com", version: "1.0" };
const readOnly = new Proxy(config, {
set() {
throw new Error("This object is read-only. You cannot change it.");
}
});
console.log(readOnly.apiUrl); // "https://api.example.com"
// readOnly.apiUrl = "something"; // throws Error
Reflect is a built-in object (not a class, not a constructor — just an object) that provides the default versions of every operation a Proxy can intercept.
Think of it as the "safe, proper way to do the thing yourself" inside a trap. Instead of manually writing target[property] = value, you can call Reflect.set(target, property, value) — which handles it correctly in all edge cases.
Why use Reflect instead of doing it manually?
- It handles inherited properties and prototypes correctly
- It returns a boolean (success/failure), making it cleaner to work with
- It mirrors exactly what JavaScript would do naturally
Key Reflect methods (they mirror the Proxy traps):
- Reflect.get(target, property) — same as target[property]
- Reflect.set(target, property, value) — same as target[property] = value
- Reflect.has(target, property) — same as property in target
- Reflect.deleteProperty(target, property) — same as delete target[property]
Here is a simple example using Reflect directly (without a Proxy):
const user = { name: "Alice", age: 25 };
// Using Reflect directly (without Proxy)
console.log(Reflect.get(user, "name")); // "Alice"
Reflect.set(user, "age", 30);
console.log(user.age); // 30
console.log(Reflect.has(user, "name")); // true
console.log(Reflect.has(user, "email")); // false
Reflect.deleteProperty(user, "age");
console.log(user.age); // undefined
On its own, Reflect is not very exciting. Its real power shows up when combined with Proxy.
The best practice is to use Reflect inside Proxy traps to handle the "default" behavior, while you add your own logic around it. This avoids bugs with prototype chains and getter/setter inheritance.
Here is a logging proxy that still works correctly:
const user = { name: "Alice", age: 25 };
const proxy = new Proxy(user, {
get(target, property, receiver) {
console.log(`Reading: ${property}`);
return Reflect.get(target, property, receiver); // safe default
},
set(target, property, value, receiver) {
console.log(`Writing: ${property} = ${value}`);
return Reflect.set(target, property, value, receiver); // safe default
}
});
proxy.name; // Logs: "Reading: name"
proxy.age = 30; // Logs: "Writing: age = 30"
console.log(user.age); // 30
The receiver parameter passed to Reflect is important — it ensures this is set correctly when the target has getters or prototype methods. Always pass it through when you use Reflect.get or Reflect.set inside a trap.
Summary of the pattern: Proxy = the interceptor. Reflect = the safe fallback. Together they give you control with correctness.
- Proxy wraps an object and lets you intercept operations like reading, writing, deleting, and more.
- You create a proxy with new Proxy(target, handler). The target is the original object; the handler has trap functions.
- Traps are methods in the handler: get, set, has, deleteProperty, apply, and more.
- If a trap is not defined, the operation passes through to the original object unchanged.
- Common uses: input validation, default values, read-only objects, logging, and observing changes.
- Reflect is a built-in object with methods that mirror every Proxy trap — Reflect.get, Reflect.set, Reflect.has, etc.
- Inside Proxy traps, use Reflect for the default behavior instead of manually doing it — it handles edge cases with prototypes and this correctly.
- Together, Proxy and Reflect give you a powerful way to add custom behavior around any object without modifying it directly.
- What is a JavaScript Proxy and what problem does it solve?
- What is the syntax for creating a Proxy?
- What is a handler trap? Name at least three common ones.
- What happens if a handler object has no traps defined?
- How does the get trap work? What arguments does it receive?
- How does the set trap work? What must it return?
- What is the difference between a Proxy and directly modifying an object?
- What is Reflect in JavaScript?
- Why should you use Reflect methods inside Proxy traps instead of doing it manually?
- What is the receiver parameter in Reflect.get and Reflect.set?
- Give a real-world example of where you would use a Proxy.
- Can you create a read-only object using Proxy? How?