Intercept reads, writes and deletes on any object to add validation, logging, defaults and reactivity.

Bishal Biswas
Self Learner
What is a Proxy?
A Proxy wraps an object and lets you intercept the basic operations on it: reading a property, writing one, deleting one, and more.
const target = { name: 'Asha', age: 28 };
const handler = {
get(obj, property) {
console.log('Reading', property);
return obj[property];
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name);
console.log(proxy.age);
Every read goes through your get function first. The original object is untouched.
The common traps
A “trap” is a handler method that intercepts one operation.
const handler = {
get(target, property, receiver) {},
set(target, property, value, receiver) {},
has(target, property) {},
deleteProperty(target, property) {},
ownKeys(target) {},
apply(target, thisArg, args) {},
construct(target, args) {}
};
get, set and has cover most real uses.
Validation
function createValidatedUser() {
return new Proxy({}, {
set(target, property, value) {
if (property === 'age') {
if (typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
if (value < 0 || value > 150) {
throw new RangeError('Age must be between 0 and 150');
}
}
if (property === 'email' && !String(value).includes('@')) {
throw new Error('Invalid email');
}
target[property] = value;
return true;
}
});
}
const user = createValidatedUser();
user.age = 28;
console.log(user.age);
try {
user.age = -5;
} catch (error) {
console.log(error.message);
}
The set trap must return true on success. Returning a falsy value throws a TypeError in strict mode.
Default values
const withDefault = (obj, fallback) => new Proxy(obj, {
get(target, property) {
return property in target ? target[property] : fallback;
}
});
const scores = withDefault({ maths: 90 }, 0);
console.log(scores.maths);
console.log(scores.physics);
Catching typos
const strict = (obj) => new Proxy(obj, {
get(target, property) {
if (typeof property === 'string' && !(property in target)) {
throw new ReferenceError(`Property "${property}" does not exist`);
}
return target[property];
}
});
const config = strict({ apiUrl: 'https://api.example.com', timeout: 5000 });
console.log(config.apiUrl);
try {
console.log(config.apiURL);
} catch (error) {
console.log(error.message);
}
Instead of silently getting undefined and debugging for an hour, you get told immediately.
Hiding private fields
const hidePrivate = (obj) => new Proxy(obj, {
get(target, property) {
if (String(property).startsWith('_')) {
throw new Error('Private property');
}
return target[property];
},
has(target, property) {
return String(property).startsWith('_') ? false : property in target;
},
ownKeys(target) {
return Reflect.ownKeys(target).filter(k => !String(k).startsWith('_'));
}
});
const account = hidePrivate({ name: 'Asha', _token: 'secret' });
console.log(account.name);
console.log('_token' in account);
console.log(Object.keys(account));
Reflect
Reflect is a plain object of functions matching every proxy trap. It gives you the default behaviour of each operation.
const obj = { a: 1 };
console.log(Reflect.get(obj, 'a'));
Reflect.set(obj, 'b', 2);
console.log(Reflect.has(obj, 'b'));
Reflect.deleteProperty(obj, 'a');
console.log(Reflect.ownKeys(obj));
Using it inside a proxy is the correct habit:
const proxy = new Proxy({ a: 1 }, {
get(target, property, receiver) {
console.log('Reading', property);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log('Writing', property);
return Reflect.set(target, property, value, receiver);
}
});
proxy.a;
proxy.b = 2;
Two reasons to prefer Reflect.get(target, property, receiver) over target[property]:
- It passes
receiver, so getters and inheritance behave correctly. Reflect.setalready returns the boolean the trap needs.
Simple reactivity
This is roughly how frameworks like Vue track changes.
function reactive(obj, onChange) {
return new Proxy(obj, {
set(target, property, value, receiver) {
const old = target[property];
const result = Reflect.set(target, property, value, receiver);
if (old !== value) {
onChange(property, old, value);
}
return result;
},
deleteProperty(target, property) {
const result = Reflect.deleteProperty(target, property);
onChange(property, 'existed', undefined);
return result;
}
});
}
const state = reactive({ count: 0 }, (key, oldValue, newValue) => {
console.log(`${key}: ${oldValue} -> ${newValue}`);
});
state.count = 1;
state.count = 2;
state.name = 'Asha';
Wrapping functions
function withLogging(fn) {
return new Proxy(fn, {
apply(target, thisArg, args) {
console.log(`Calling ${target.name} with`, args);
const result = Reflect.apply(target, thisArg, args);
console.log('Returned', result);
return result;
}
});
}
const add = withLogging(function add(a, b) {
return a + b;
});
add(2, 3);
Costs and limits
- Slower. Every intercepted operation runs your JavaScript. Do not proxy something in a hot loop.
- Not perfectly transparent. Proxies cannot intercept private
#fields, and internal slots on built ins likeMaporDateneed extra care. - Harder to debug. A stack trace through a proxy is less obvious.
Use a proxy when the alternative is worse: framework reactivity, developer facing validation, mocking in tests, or API clients that build URLs from property names.
Quick recap
- A Proxy intercepts operations on a target object using traps.
get,set,hasanddeletePropertycover most needs.- The
settrap must returntrueon success. Reflectprovides the default behaviour for every trap.- Prefer
Reflect.get(target, prop, receiver)so getters and inheritance work. - Great for validation, defaults, typo detection and reactivity.
- They cost performance, so keep them out of hot paths.
// A Proxy intercepts operations on a target object
const user = new Proxy({}, {
set(target, property, value) {
if (property === 'age') {
if (typeof value !== 'number') throw new TypeError('Age must be a number');
if (value < 0 || value > 150) throw new RangeError('Age out of range');
}
target[property] = value;
return true; // set must return true on success
}
});
user.age = 28;
console.log(user.age);
try { user.age = -5; } catch (e) { console.log(e.message); }
// Default values for anything missing
const withDefault = (obj, fallback) => new Proxy(obj, {
get(target, property) {
return property in target ? target[property] : fallback;
}
});
const scores = withDefault({ maths: 90 }, 0);
console.log(scores.maths, scores.physics); // 90 0
// Catch typos instead of silently getting undefined
const strict = obj => new Proxy(obj, {
get(target, property) {
if (typeof property === 'string' && !(property in target)) {
throw new ReferenceError('No such property: ' + property);
}
return target[property];
}
});
const config = strict({ apiUrl: 'https://api.example.com' });
console.log(config.apiUrl);
try { config.apiURL; } catch (e) { console.log(e.message); }
// Reflect gives the default behaviour, and passes receiver correctly
const logged = new Proxy({ a: 1 }, {
get(target, property, receiver) {
console.log('Reading', property);
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
console.log('Writing', property);
return Reflect.set(target, property, value, receiver);
}
});
logged.a;
logged.b = 2;
// Simple reactivity, roughly how frameworks track state changes
function reactive(obj, onChange) {
return new Proxy(obj, {
set(target, property, value, receiver) {
const old = target[property];
const result = Reflect.set(target, property, value, receiver);
if (old !== value) onChange(property, old, value);
return result;
}
});
}
const state = reactive({ count: 0 }, (key, from, to) =>
console.log(key + ': ' + from + ' -> ' + to));
state.count = 1;
state.count = 2;
// The apply trap wraps function calls
const add = new Proxy((a, b) => a + b, {
apply(target, thisArg, args) {
console.log('Called with', args);
return Reflect.apply(target, thisArg, args);
}
});
console.log(add(2, 3));
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready