The Proxy object
Proxy is a wrapper around an object that lets you intercept operations on it. Put simply, Proxy gives you a way to stand between your code and the object so you can control what happens when properties are read, changed or otherwise touched.
Theory
TL;DR
new Proxy(target, handler)creates a wrapper around the original object.handleris an object with traps, each of which intercepts one operation.- The main traps are
get,set,has,deleteProperty,ownKeys,applyandconstruct. - The
setanddeletePropertytraps must returntrue, otherwise strict mode throws aTypeError. Reflectholds methods matching every trap and performs the default behaviour.- Typical uses: validation, logging, default values, property protection, Vue 3 reactivity, i18n with a fallback.
Quick example
const user = { name: 'Tim', age: 25 };
const proxy = new Proxy(user, {
get(target, prop) {
console.log(`Reading property "${prop}"`);
return target[prop];
},
set(target, prop, value) {
console.log(`Changing "${prop}" to "${value}"`);
target[prop] = value;
return true; // you must return true, otherwise there will be an error
}
});
console.log(proxy.name); // Reading property "name" -> "Tim"
proxy.age = 26; // Changing "age" to "26"Syntax
const proxy = new Proxy(target, handler);| Parameter | Description |
|---|---|
target | the original object the proxy is built over |
handler | an object with traps that intercept operations |
A proxy does not copy the object: every operation eventually reaches target, as long as the trap passes it through. If there is no trap for an operation, it runs exactly as if the Proxy did not exist.
Practical trap examples
Validation on write. The set trap checks the type and refuses to store garbage in the object.
const user = new Proxy({}, {
set(target, prop, value) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
target[prop] = value;
return true;
}
});
user.age = 30; // ok
user.age = 'thirty'; // TypeErrorDefault values. The get trap returns a stand-in value for properties that do not exist.
const config = new Proxy({}, {
get(target, prop) {
return prop in target ? target[prop] : '<not set>';
}
});
console.log(config.host); // <not set>
config.host = 'localhost';
console.log(config.host); // localhostProtection from deletion. The deleteProperty trap forbids removing a key field.
const secure = new Proxy({ id: 1, name: 'Admin' }, {
deleteProperty(target, prop) {
if (prop === 'id') {
console.log('Deletion is forbidden!');
return false;
}
delete target[prop];
return true;
}
});
delete secure.name; // gets deleted
delete secure.id; // Deletion is forbidden!Reflect and the default behaviour
Reflect is a set of methods matching all the Proxy traps. It lets you invoke the default behaviour whenever you need it.
function createLoggedObject(obj) {
return new Proxy(obj, {
get(target, prop, receiver) {
console.log(`Access to property "${prop}"`);
return Reflect.get(target, prop, receiver); // the standard behaviour
}
});
}
const user = createLoggedObject({ name: 'Tim' });
console.log(user.name);
// Access to property "name"Without Reflect you would have to write target[prop] by hand, and that can break inheritance and this: a getter on the prototype receives the wrong receiver and reads fields from target instead of from the proxy.
Available traps
| Trap | What it intercepts | Reflect equivalent |
|---|---|---|
get | Reading a property | Reflect.get() |
set | Writing a property | Reflect.set() |
has | The prop in obj check | Reflect.has() |
deleteProperty | Deletion, delete obj[prop] | Reflect.deleteProperty() |
ownKeys | Getting all keys (Object.keys, for...in) | Reflect.ownKeys() |
getOwnPropertyDescriptor | A property descriptor | Reflect.getOwnPropertyDescriptor() |
defineProperty | Creating or changing a property | Reflect.defineProperty() |
preventExtensions | Forbidding new properties | Reflect.preventExtensions() |
isExtensible | Checking whether the object is extensible | Reflect.isExtensible() |
getPrototypeOf | Getting the prototype | Reflect.getPrototypeOf() |
setPrototypeOf | Changing the prototype | Reflect.setPrototypeOf() |
apply | Calling a function | Reflect.apply() |
construct | Calling through new | Reflect.construct() |
You can proxy not only a plain object but a function too, and then the apply and construct traps come into play:
function sum(a, b) {
return a + b;
}
const proxySum = new Proxy(sum, {
apply(target, thisArg, args) {
console.log(`Calling the function with arguments: ${args}`);
return Reflect.apply(target, thisArg, args);
}
});
console.log(proxySum(2, 3)); // Calling the function with arguments: 2,3 -> 5Where Proxy is genuinely useful
| Scenario | Example of use |
|---|---|
| Data validation | Type checking on write |
| Debugging and logging | Intercepting get and set for analysis |
| Reactivity (Vue 3) | Tracking property changes |
| Protection and restrictions | Forbidding deletion or modification of properties |
| Localisation and key access | i18n, fallback data |
| API wrappers | "Smart" dynamic objects, a REST API built on the fly for instance |
An example of a dynamic structure for i18n: instead of an error or undefined the user sees the key itself, and translators immediately notice what is missing.
const dictionary = {
hello: 'Hello',
world: 'world'
};
const i18n = new Proxy(dictionary, {
get(target, key) {
if (key in target) return target[key];
return `[${key}]`; // substitute the key instead of a translation
}
});
console.log(i18n.hello); // Hello
console.log(i18n.missingKey); // [missingKey]A short summary of the terms:
| Term | Description |
|---|---|
Proxy | An object that intercepts operations on another object |
target | The original object (the target) |
handler | An object with traps (intercepting methods) |
trap | A method intercepting a particular operation (get, set and so on) |
Reflect | Lets you invoke the standard default behaviour |
Common mistakes
- Forgetting
return trueinsetordeleteProperty. The trap has to tell the engine that the operation succeeded. In strict mode, and modules are always strict, returningfalseorundefinedleads to aTypeError. - Writing
target[prop]instead ofReflect.get(target, prop, receiver). Thereceiveris lost, so getters from the prototype get the wrongthis. - Assuming the proxy changes the object itself. Traps fire only for accesses that go through the proxy. If a reference to
targetis still around somewhere, every check is bypassed. - Breaking the object invariants. For a non-configurable property the
gettrap cannot return a different value, and for a non-extensible objectownKeyscannot invent new keys: the engine throws aTypeError. - Wrapping everything in a proxy. Every operation goes through your code, so a proxy is noticeably slower than a plain object in a hot loop.
- Expecting
Proxyto be polyfillable. Interception is implemented at the engine level, so there is no substitute for old environments without support.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.