I avoided JavaScript Proxy for years because every tutorial made it look like academic nonsense. Traps? Handlers? Get and set interceptors? Nobody writes code like that in real projects. Except I was wrong. Proxy is one of those tools that sounds over-engineered until you need it, and then you wonder how you lived without it.
A Proxy wraps an object and lets you intercept any operation on it. Read a property, write a property, delete a property, check if a property exists. All of those can be intercepted and custom logic applied. Think of it as middleware for objects.
The Basic Syntax
const handler = {
get(target, prop) {
console.log(`Someone read ${prop}`);
return target[prop];
}
};
const person = { name: 'Davide', city: 'London' };
const proxy = new Proxy(person, handler);
proxy.name;
// Console: Someone read name
// Returns: 'Davide'That's it. You create a handler object with trap methods, wrap your target with new Proxy, and every access goes through your handler first. The original object stays untouched.
The Traps You Actually Need
There are 13 traps. You will use three of them. Maybe four if you're feeling fancy.
const handler = {
// Intercept property reads
get(target, prop) {
if (!(prop in target)) {
throw new Error(`Property '${prop}' does not exist`);
}
return target[prop];
},
// Intercept property writes
set(target, prop, value) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('age must be a number');
}
target[prop] = value;
return true; // required!
},
// Intercept property deletion
deleteProperty(target, prop) {
if (prop === 'id') {
throw new Error('Cannot delete id');
}
delete target[prop];
return true;
}
};The get trap fires when someone reads a property. The set trap fires when someone writes one. deleteProperty fires on delete. The return value of set and deleteProperty must be true or you get a TypeError in strict mode. I learned that the hard way.
Validation Without Proxy Is Ugly
Here's what validation looks like without Proxy. Every setter has checks, every constructor has guards, everything is repetitive.
// Without Proxy - manual validation everywhere
class User {
set name(value) {
if (typeof value !== 'string') throw new TypeError('name must be string');
if (value.length < 2) throw new Error('name too short');
this._name = value;
}
set age(value) {
if (typeof value !== 'number') throw new TypeError('age must be number');
if (value < 0) throw new Error('age cannot be negative');
this._age = value;
}
// ... repeat for every property
}With Proxy you write validation once and it applies everywhere.
const rules = {
name: { type: 'string', minLength: 2 },
age: { type: 'number', min: 0 },
email: { type: 'string', pattern: /@/ }
};
const validator = {
set(target, prop, value) {
const rule = rules[prop];
if (rule) {
if (rule.type && typeof value !== rule.type) {
throw new TypeError(`${prop} must be ${rule.type}`);
}
if (rule.min !== undefined && value < rule.min) {
throw new Error(`${prop} must be at least ${rule.min}`);
}
if (rule.minLength && value.length < rule.minLength) {
throw new Error(`${prop} must be at least ${rule.minLength} chars`);
}
if (rule.pattern && !rule.pattern.test(value)) {
throw new Error(`${prop} format invalid`);
}
}
target[prop] = value;
return true;
}
};
const user = new Proxy({}, validator);
user.name = 'Davide'; // OK
user.age = -5; // Error: age must be at least 0
user.email = 'nope'; // Error: email format invalidOne handler, all properties covered. Add a rule to the rules object and you're done. No class modifications, no setter changes.
Default Values With Get
This is where I actually use Proxy in production. Nested config objects with defaults.
const defaults = {
port: 3000,
host: 'localhost',
debug: false,
retries: 3
};
const config = new Proxy({ port: 8080 }, {
get(target, prop) {
return prop in target ? target[prop] : defaults[prop];
}
});
config.port; // 8080 ( from target )
config.host; // 'localhost' ( from defaults )
config.debug; // false ( from defaults )
config.retries; // 3 ( from defaults )No more const port = process.env.PORT || 3000 littered everywhere. One proxy, one defaults object, done.
Logging And Debugging
I use a transparent logging proxy during development. Every property access gets logged. Saves me from adding console.log to fifteen places and then removing them.
function logProxy(target, label = 'Object') {
return new Proxy(target, {
get(obj, prop) {
console.log(`${label}.${prop} was read`);
const value = obj[prop];
return typeof value === 'function' ? value.bind(obj) : value;
},
set(obj, prop, value) {
console.log(`${label}.${prop} set to ${JSON.stringify(value)}`);
obj[prop] = value;
return true;
}
});
}
const api = logProxy({ users: [], count: 0 }, 'api');
api.users; // api.users was read
api.count = 5; // api.count set to 5The bind call on the function is important. Without it, methods lose their this context and everything breaks in confusing ways. Ask me how I know.
Revocable Proxies
Sometimes you want a proxy that can be turned off. Proxy.revocable gives you that.
const { proxy, revoke } = Proxy.revocable(sensitiveData, {
get(target, prop) {
// Access control here
return target[prop];
}
});
// Later, after use:
revoke();
// Now any access on proxy throws TypeError
proxy.name; // TypeError: illegal operation attempted on a revoked proxyI use this for API responses that should expire. Pass the proxy to a consumer, revoke after the timeout, and nobody can read the data anymore. Clean and simple.
What Not To Do
Don't proxy everything. Proxy adds overhead. Every property access goes through the handler. In a tight loop with millions of iterations, that adds up. Use it for config objects, API wrappers, and debugging. Not for your render loop.
Don't use Proxy when a plain function will do. If you just need get/set on one property, write a getter and setter. Proxy is for when you need to intercept all properties or when you don't know the property names in advance.
Conclusion
Proxy is not some abstract pattern you'll never use. It solves real problems: validation, defaults, logging, access control, and revocable access. The syntax is minimal, the traps are well-named, and it works in every modern browser and Node.js.
I wish I had started using it sooner. :)
MDN Proxy docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy