Back to Blog
JavaScriptProxyMeta-programmingHandlers

JavaScript Proxy Handler Guide

Master JavaScript Proxy handlers for intercepting and customizing object operations.

B
Bootspring Team
Engineering
May 3, 2019
6 min read

Proxy handlers intercept operations on objects, enabling powerful meta-programming patterns. Here's how to use each trap effectively.

Basic Structure#

1const target = { name: 'Original' }; 2 3const handler = { 4 get(target, property, receiver) { 5 console.log(`Getting ${property}`); 6 return Reflect.get(target, property, receiver); 7 }, 8 9 set(target, property, value, receiver) { 10 console.log(`Setting ${property} to ${value}`); 11 return Reflect.set(target, property, value, receiver); 12 }, 13}; 14 15const proxy = new Proxy(target, handler); 16 17proxy.name; // "Getting name" 18proxy.age = 25; // "Setting age to 25"

get Trap#

1const handler = { 2 get(target, property, receiver) { 3 // Property doesn't exist 4 if (!(property in target)) { 5 return `Property '${property}' not found`; 6 } 7 8 // Transform value 9 const value = Reflect.get(target, property, receiver); 10 if (typeof value === 'string') { 11 return value.toUpperCase(); 12 } 13 14 return value; 15 }, 16}; 17 18const obj = new Proxy({ name: 'john' }, handler); 19console.log(obj.name); // 'JOHN' 20console.log(obj.missing); // "Property 'missing' not found"

set Trap#

1const handler = { 2 set(target, property, value, receiver) { 3 // Validation 4 if (property === 'age' && (typeof value !== 'number' || value < 0)) { 5 throw new TypeError('Age must be a positive number'); 6 } 7 8 // Transformation 9 if (property === 'name' && typeof value === 'string') { 10 value = value.trim(); 11 } 12 13 // Logging 14 console.log(`${property} changed to ${value}`); 15 16 return Reflect.set(target, property, value, receiver); 17 }, 18}; 19 20const user = new Proxy({}, handler); 21user.name = ' John '; // Trimmed to 'John' 22user.age = 30; // OK 23// user.age = -5; // TypeError

has Trap#

1const handler = { 2 has(target, property) { 3 // Hide private properties 4 if (property.startsWith('_')) { 5 return false; 6 } 7 8 return Reflect.has(target, property); 9 }, 10}; 11 12const obj = new Proxy({ name: 'John', _secret: 'hidden' }, handler); 13console.log('name' in obj); // true 14console.log('_secret' in obj); // false (hidden)

deleteProperty Trap#

1const handler = { 2 deleteProperty(target, property) { 3 // Prevent deletion of certain properties 4 if (property === 'id') { 5 console.log('Cannot delete id'); 6 return false; 7 } 8 9 // Log deletion 10 console.log(`Deleting ${property}`); 11 12 return Reflect.deleteProperty(target, property); 13 }, 14}; 15 16const obj = new Proxy({ id: 1, name: 'John' }, handler); 17delete obj.name; // "Deleting name" - succeeds 18delete obj.id; // "Cannot delete id" - fails

ownKeys Trap#

1const handler = { 2 ownKeys(target) { 3 // Filter out private properties 4 return Reflect.ownKeys(target).filter( 5 (key) => !String(key).startsWith('_') 6 ); 7 }, 8}; 9 10const obj = new Proxy( 11 { name: 'John', age: 30, _private: 'hidden' }, 12 handler 13); 14 15console.log(Object.keys(obj)); // ['name', 'age'] 16console.log(Object.entries(obj)); // [['name', 'John'], ['age', 30]]

apply Trap (Functions)#

1const handler = { 2 apply(target, thisArg, argumentsList) { 3 console.log(`Calling function with args: ${argumentsList}`); 4 5 // Modify arguments 6 const modifiedArgs = argumentsList.map((arg) => 7 typeof arg === 'number' ? arg * 2 : arg 8 ); 9 10 return Reflect.apply(target, thisArg, modifiedArgs); 11 }, 12}; 13 14const double = new Proxy( 15 (a, b) => a + b, 16 handler 17); 18 19console.log(double(2, 3)); // Calling..., returns 10 (4 + 6)

construct Trap (Classes)#

1const handler = { 2 construct(target, argumentsList, newTarget) { 3 console.log(`Creating new instance with: ${argumentsList}`); 4 5 // Modify constructor arguments 6 const instance = Reflect.construct(target, argumentsList, newTarget); 7 8 // Add extra properties 9 instance.createdAt = new Date(); 10 11 return instance; 12 }, 13}; 14 15class User { 16 constructor(name) { 17 this.name = name; 18 } 19} 20 21const ProxiedUser = new Proxy(User, handler); 22const user = new ProxiedUser('John'); 23console.log(user.createdAt); // Date object

getOwnPropertyDescriptor Trap#

1const handler = { 2 getOwnPropertyDescriptor(target, property) { 3 const descriptor = Reflect.getOwnPropertyDescriptor(target, property); 4 5 // Hide private properties 6 if (property.startsWith('_')) { 7 return undefined; 8 } 9 10 // Modify descriptor 11 if (descriptor) { 12 descriptor.enumerable = true; 13 } 14 15 return descriptor; 16 }, 17}; 18 19const obj = new Proxy({ name: 'John', _secret: 'hidden' }, handler); 20console.log(Object.getOwnPropertyDescriptor(obj, 'name')); 21console.log(Object.getOwnPropertyDescriptor(obj, '_secret')); // undefined

defineProperty Trap#

1const handler = { 2 defineProperty(target, property, descriptor) { 3 // Prevent non-writable properties 4 if (descriptor.writable === false) { 5 console.log('Non-writable properties not allowed'); 6 return false; 7 } 8 9 // Validate property names 10 if (property.startsWith('_')) { 11 console.log('Properties cannot start with _'); 12 return false; 13 } 14 15 return Reflect.defineProperty(target, property, descriptor); 16 }, 17}; 18 19const obj = new Proxy({}, handler); 20 21Object.defineProperty(obj, 'name', { 22 value: 'John', 23 writable: true, 24}); // OK 25 26Object.defineProperty(obj, '_private', { 27 value: 'hidden', 28}); // Fails

getPrototypeOf / setPrototypeOf Traps#

1const handler = { 2 getPrototypeOf(target) { 3 console.log('Getting prototype'); 4 return Reflect.getPrototypeOf(target); 5 }, 6 7 setPrototypeOf(target, prototype) { 8 // Prevent prototype changes 9 console.log('Prototype is immutable'); 10 return false; 11 }, 12}; 13 14const obj = new Proxy({}, handler); 15Object.getPrototypeOf(obj); // "Getting prototype" 16Object.setPrototypeOf(obj, null); // Fails - "Prototype is immutable"

isExtensible / preventExtensions Traps#

1const handler = { 2 isExtensible(target) { 3 console.log('Checking extensibility'); 4 return Reflect.isExtensible(target); 5 }, 6 7 preventExtensions(target) { 8 console.log('Preventing extensions'); 9 return Reflect.preventExtensions(target); 10 }, 11}; 12 13const obj = new Proxy({}, handler); 14console.log(Object.isExtensible(obj)); // true 15Object.preventExtensions(obj); 16console.log(Object.isExtensible(obj)); // false

Complete Observable Object#

1function createObservable(target, onChange) { 2 const handler = { 3 get(target, property, receiver) { 4 const value = Reflect.get(target, property, receiver); 5 6 // Return proxy for nested objects 7 if (typeof value === 'object' && value !== null) { 8 return createObservable(value, (path, newVal, oldVal) => { 9 onChange(`${property}.${path}`, newVal, oldVal); 10 }); 11 } 12 13 return value; 14 }, 15 16 set(target, property, value, receiver) { 17 const oldValue = target[property]; 18 const result = Reflect.set(target, property, value, receiver); 19 20 if (result && oldValue !== value) { 21 onChange(property, value, oldValue); 22 } 23 24 return result; 25 }, 26 27 deleteProperty(target, property) { 28 const oldValue = target[property]; 29 const result = Reflect.deleteProperty(target, property); 30 31 if (result) { 32 onChange(property, undefined, oldValue); 33 } 34 35 return result; 36 }, 37 }; 38 39 return new Proxy(target, handler); 40} 41 42// Usage 43const state = createObservable( 44 { user: { name: 'John' }, count: 0 }, 45 (path, newVal, oldVal) => { 46 console.log(`${path}: ${oldVal} -> ${newVal}`); 47 } 48); 49 50state.count = 1; // count: 0 -> 1 51state.user.name = 'Jane'; // user.name: John -> Jane

Best Practices#

Trap Usage: ✓ Use Reflect methods for defaults ✓ Return correct values ✓ Maintain invariants ✓ Handle edge cases Performance: ✓ Keep handlers simple ✓ Cache proxies when possible ✓ Avoid deep nesting ✓ Consider alternatives for hot paths Patterns: ✓ Validation and access control ✓ Logging and debugging ✓ Observable objects ✓ Default values Avoid: ✗ Breaking object invariants ✗ Infinite recursion ✗ Hiding errors silently ✗ Over-engineering simple cases

Conclusion#

Proxy handlers provide fine-grained control over object operations through traps like get, set, has, and deleteProperty. Always use Reflect methods to maintain default behavior and return appropriate values. Proxies are powerful for validation, observation, access control, and virtualization patterns, but should be used judiciously as they add overhead and complexity.

Share this article

Help spread the word about Bootspring