Proxy lets us wrap an object and intercept operations on it — like reading a property, setting a value, checking if a key exists, etc. Think of it as putting a guard in front of an object that can inspect and modify every interaction.
How Proxy Works
A Proxy takes two arguments: the target object and a handler with traps (functions that intercept operations).
const user = { name: 'Manish', age: 25 };
const proxy = new Proxy(user, {
get(target, prop) {
console.log(`Reading "${prop}"`);
return target[prop];
},
set(target, prop, value) {
console.log(`Setting "${prop}" to ${value}`);
target[prop] = value;
return true; // must return true for success
}
});
proxy.name; // logs: Reading "name" → 'Manish'
proxy.age = 26; // logs: Setting "age" to 26
Common Handler Traps
Here are the traps we use most often:
get(target, prop)— reading a propertyset(target, prop, value)— writing a propertyhas(target, prop)— theinoperatordeleteProperty(target, prop)— thedeleteoperatorapply(target, thisArg, args)— calling a function
Use Case: Validation Proxy
One of the most practical uses — we can validate values before they’re set on an object.
const validator = {
set(target, prop, value) {
if (prop === 'age') {
if (typeof value !== 'number') throw TypeError('Age must be a number');
if (value < 0 || value > 150) throw RangeError('Age must be 0-150');
}
target[prop] = value;
return true;
}
};
const person = new Proxy({}, validator);
person.name = 'Manish'; // works fine
person.age = 25; // works fine
// person.age = -5; // RangeError: Age must be 0-150
// person.age = 'old'; // TypeError: Age must be a number
Use Case: Logging Proxy
We can wrap any object to log every access — useful for debugging.
function withLogging(obj) {
return new Proxy(obj, {
get(target, prop) {
console.log(`[GET] ${prop} → ${target[prop]}`);
return target[prop];
},
set(target, prop, value) {
console.log(`[SET] ${prop} = ${value}`);
target[prop] = value;
return true;
}
});
}
const config = withLogging({ debug: false, port: 3000 });
config.debug; // [GET] debug → false
config.port = 8080; // [SET] port = 8080
Reflect
Reflect is a built-in object that provides methods matching every Proxy trap. Instead of directly doing target[prop], we can use Reflect.get(target, prop) — it’s cleaner and always returns the correct default behavior.
const proxy = new Proxy(user, {
get(target, prop, receiver) {
console.log(`Accessing ${prop}`);
return Reflect.get(target, prop, receiver); // proper default
},
set(target, prop, value, receiver) {
console.log(`Setting ${prop}`);
return Reflect.set(target, prop, value, receiver);
},
has(target, prop) {
console.log(`Checking if "${prop}" exists`);
return Reflect.has(target, prop);
}
});
Why use Reflect instead of target[prop]? Because Reflect methods return success/failure booleans, handle edge cases with inheritance correctly (via the receiver parameter), and map 1-to-1 with every Proxy trap.
Real-World Usage
This isn’t just a theoretical concept. Vue.js 3 uses Proxy to power its reactivity system. When we change a reactive property, Vue’s Proxy trap detects the change and triggers a re-render. Before Vue 3, they used Object.defineProperty which had limitations (couldn’t detect new property additions or array index changes).
In simple language, Proxy is like a security guard for objects — every time someone tries to read, write, or check something on the object, the guard can inspect it, modify it, or block it. Reflect just gives us a clean way to do the “normal” thing inside those guard functions.