Proxy & Reflect
Intercept and customise fundamental object operations — get, set, delete, function calls, and more — with Proxy traps and the Reflect API.
A Proxy only guards the object you wrap, not the objects inside it. Reading config.db through a read-only proxy hands back the plain inner object, and writes to that object go straight through with no error. The same applies to validation and change-tracking proxies: nested changes skip your traps entirely. Object.freeze is shallow in exactly the same way.
function readonly(target) {
return new Proxy(target, {
set(_, key) {
throw new TypeError(`Cannot set "${key}" — object is read-only`);
},
deleteProperty(_, key) {
throw new TypeError(`Cannot delete "${key}" — object is read-only`);
}
});
}
const config = readonly({ endpoint: "/api", db: { password: "s3cret" } });
config.db.password = "hacked"; // no error: config.db is the plain inner object
console.log(config.db.password);
// hackedWrapping objects to intercept their operations
A Proxy sits in front of an object. Every time code tries to read, write, or delete a property on the proxy, your handler function gets called first — giving you a chance to validate, log, transform, or block the operation.
Creating a Proxy
// new Proxy(target, handler)
// target — the real object being wrapped
// handler — an object with "trap" methods
const person = { name: "Alice", age: 30 };
const proxy = new Proxy(person, {
get(target, key) {
// Called every time proxy.someProperty is read
console.log(`→ Reading "${key}"`);
return target[key]; // return the actual value
},
set(target, key, value) {
// Called every time proxy.someProperty = value
console.log(`→ Writing "${key}" = ${value}`);
target[key] = value;
return true; // must return true to indicate success
}
});
proxy.name; // logs: → Reading "name"
proxy.age = 31; // logs: → Writing "age" = 31The most useful trap: validation
function createValidatedUser(data) {
return new Proxy(data, {
set(target, key, value) {
if (key === "age") {
if (typeof value !== "number") throw new TypeError("age must be a number");
if (value < 0 || value > 150) throw new RangeError("age must be 0–150");
}
if (key === "email" && !value.includes("@")) {
throw new Error("Invalid email address");
}
target[key] = value;
return true;
}
});
}
const user = createValidatedUser({ name: "Bob" });
user.name = "Bob"; // fine
user.age = 25; // fine
// user.age = -5; // throws: age must be 0–150
// user.email = "notvalid"; // throws: Invalid email addressReflect — the default implementation
Inside a trap, use Reflect to perform the operation normally.
This is cleaner than manually doing target[key] because Reflect handles
edge cases correctly (inherited properties, getters/setters, etc.).
const handler = {
get(target, key, receiver) {
console.log(`Reading "${key}"`);
return Reflect.get(target, key, receiver); // same as: target[key]
},
set(target, key, value, receiver) {
console.log(`Writing "${key}"`);
return Reflect.set(target, key, value, receiver); // same as: target[key] = value
}
};
const proxy = new Proxy({ x: 1 }, handler);
proxy.x; // "Reading x"
proxy.y = 2; // "Writing y"Official Sources
The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.