Proxy & Reflect in JavaScript
Proxy lets you intercept operations on an object or function. Reflect provides methods for performing many of those operations using JavaScript's standard behavior. Together, they are useful for validation, safe defaults, logging, and controlled access.
A Proxy sits between your code and an object or function. It can intercept operations such as reading, writing, deleting, or calling the target.
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 config = { theme: "dark", fontSize: 16 };
console.log(config.theme); // "dark"
config.fontSize = -5; // No error! Bad data accepted silently.
console.log(config.fontSize); // -5
Proxy lets you add a layer of control around an object. Depending on the handler, it can pass operations through, block them, or change how they behave.
The syntax for creating a Proxy is:
const proxy = new Proxy(target, handler)
- target - the original object or function
- handler - an object containing trap methods
If the handler is empty {}, the proxy just passes everything through - no changes at all.
const config = { theme: "dark", fontSize: 16 };
// An empty handler -- everything passes through unchanged
const proxy = new Proxy(config, {});
console.log(proxy.theme); // "dark" -- same as config.theme
proxy.fontSize = 20;
console.log(config.fontSize); // 20 -- 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 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)
- ownKeys - intercepts requests for the target's property keys
- getOwnPropertyDescriptor - intercepts requests for a property descriptor
- construct - intercepts constructor calls when the target is a function
Functions can be proxied too. For example, apply can log a function call, and construct can intercept new SomeFunction().
The get trap runs when a property is read through the Proxy:
const config = { theme: "dark", fontSize: 16 };
const proxy = new Proxy(config, {
get(target, property) {
console.log(`Someone read: ${property}`);
return target[property]; // return the real value
}
});
console.log(proxy.theme);
// Logs: "Someone read: theme"
// Then: "dark"
console.log(proxy.language);
// Logs: "Someone read: language"
// Then: undefined (property doesn't exist)
Here is a practical validation example for a settings object. The set trap checks the new value before saving it. Valid data is stored with Reflect.set; invalid data throws an error that the caller can catch.
const config = { theme: "dark", fontSize: 16 };
const proxy = new Proxy(config, {
set(target, property, value) {
if (property === "fontSize" && typeof value !== "number") {
throw new TypeError("fontSize must be a number!");
}
if (property === "fontSize" && (value < 8 || value > 72)) {
throw new RangeError("fontSize must be between 8 and 72!");
}
return Reflect.set(target, property, value); // apply the normal write
}
});
try {
proxy.fontSize = 20; // valid: the value is stored
console.log(config.fontSize); // 20
proxy.fontSize = "large"; // invalid: TypeError
} catch (error) {
console.log(error.message); // fontSize must be a number!
}
try {
proxy.fontSize = 2; // invalid: RangeError
} catch (error) {
console.log(error.message); // fontSize must be between 8 and 72!
}
- Assignment starts the trap: proxy.fontSize = value calls set with the target, property name, and new value.
- Validation runs first: the trap rejects non-numbers and numbers outside the allowed range.
- The normal write still happens: Reflect.set stores valid data and returns whether the operation succeeded.
If a trap is not defined in the handler, the operation works normally on the target. Proxy traps cannot report impossible results that violate JavaScript's rules for the target object.
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 operations to log what is happening, which can help with debugging.
Read-only objects - intercept set to prevent top-level 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
This example is shallow. If config contains nested objects, those nested objects can still be changed unless you wrap them in their own proxies too.
Reflect is a built-in JavaScript object with methods for performing common object operations. It is not a class or constructor.
Inside a Proxy trap, Reflect is commonly used to perform the normal operation while preserving JavaScript's standard behavior. For example, Reflect.set(target, property, value) performs the normal property write.
Why use Reflect instead of doing it manually?
- Its methods mirror corresponding JavaScript object operations
- Methods such as Reflect.set return a useful success value
- It provides a clear way to pass the normal operation through a Proxy trap
Key Reflect methods include:
- Reflect.get(target, property) - performs a property read
- Reflect.set(target, property, value) - performs a property write
- Reflect.has(target, property) - checks whether a property exists
- Reflect.deleteProperty(target, property) - deletes a property
- Reflect.ownKeys(target) and Reflect.getOwnPropertyDescriptor(target, property) work with keys and property details
- Reflect.apply(fn, thisArg, args) and Reflect.construct(fn, args) work with function calls and constructor calls
Here is a simple example using Reflect directly (without a Proxy):
const config = { theme: "dark", fontSize: 16 };
// Using Reflect directly (without Proxy)
console.log(Reflect.get(config, "theme")); // "dark"
Reflect.set(config, "fontSize", 20);
console.log(config.fontSize); // 20
console.log(Reflect.has(config, "theme")); // true
console.log(Reflect.has(config, "language")); // false
Reflect.deleteProperty(config, "fontSize");
console.log(config.fontSize); // undefined
On its own, Reflect is not very exciting. Its real power shows up when combined with Proxy.
Proxy intercepts the operation. Reflect performs the normal operation. This combination lets you add logic while keeping JavaScript's usual behavior for the underlying object.
Here is a logging proxy that still works correctly:
const config = { theme: "dark", fontSize: 16 };
const proxy = new Proxy(config, {
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.theme; // Logs: "Reading: theme"
proxy.fontSize = 20; // Logs: "Writing: fontSize = 20"
console.log(config.fontSize); // 20
Advanced: The receiver Parameter
The receiver parameter is important when a Proxy works with getters and setters. It determines the object used as this when the getter or setter is called through the Proxy.
In short: Proxy intercepts the operation, and Reflect performs the normal operation.
Advanced: Proxy.revocable() creates a Proxy that can later be disabled.
- Proxy is an advanced feature: it 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 for many fundamental operations such as Reflect.get, Reflect.set, Reflect.has, Reflect.ownKeys, and more.
- Inside Proxy traps, Reflect is commonly used to perform the matching standard operation.
- Proxies can wrap objects or functions, but they do not copy the target. Use them when the added control is worth the extra complexity.
- 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?