The Proxy object is one of the most powerful metaprogramming features in JavaScript. A Proxy wraps another object (the target) and intercepts fundamental operations on it โ property reads, writes, deletions, function calls, in checks, and more. Every intercepted operation is handled by a trap function you define. The Reflect API is the companion to Proxy โ it provides the same operations as Proxy traps as standalone functions, making it easy to implement the default behaviour inside a trap. Together, Proxy and Reflect enable validation, logging, computed properties, deep reactivity, and operator overloading patterns.
Proxy Traps Reference
| Trap | Intercepts | Default via Reflect |
|---|---|---|
get(target, prop, receiver) |
obj.prop / obj[prop] |
Reflect.get(target, prop, receiver) |
set(target, prop, value, receiver) |
obj.prop = value |
Reflect.set(target, prop, value, receiver) |
has(target, prop) |
'prop' in obj |
Reflect.has(target, prop) |
deleteProperty(target, prop) |
delete obj.prop |
Reflect.deleteProperty(target, prop) |
apply(target, thisArg, args) |
Function call fn() |
Reflect.apply(target, thisArg, args) |
construct(target, args, newTarget) |
new Fn() |
Reflect.construct(target, args, newTarget) |
ownKeys(target) |
Object.keys(), for...in |
Reflect.ownKeys(target) |
getPrototypeOf(target) |
Object.getPrototypeOf() |
Reflect.getPrototypeOf(target) |
defineProperty(target, prop, desc) |
Object.defineProperty() |
Reflect.defineProperty(target, prop, desc) |
Reflect Methods
| Reflect Method | Equivalent To | Advantage |
|---|---|---|
Reflect.get(obj, key) |
obj[key] |
Returns undefined instead of throwing |
Reflect.set(obj, key, val) |
obj[key] = val |
Returns boolean success โ doesn’t throw |
Reflect.has(obj, key) |
key in obj |
Functional form โ usable as callback |
Reflect.ownKeys(obj) |
Object.getOwnPropertyNames + Symbols |
All own keys including Symbols |
Reflect.apply(fn, this, args) |
fn.apply(this, args) |
Always works โ even if apply is overridden |
Reflect.construct(Fn, args) |
new Fn(...args) |
Dynamic construction without spread |
Reflect.deleteProperty(obj, key) |
delete obj[key] |
Returns boolean โ doesn’t throw in strict mode |
set trap must return true to indicate success, or false to indicate failure. In strict mode, returning false from a set trap causes a TypeError to be thrown at the call site. Always return Reflect.set(target, prop, value, receiver) as the default โ this correctly propagates the success/failure signal and handles prototype chain interactions via receiver.Reflect inside Proxy traps to implement the default behaviour while adding your custom logic around it. Reflect.get(target, prop, receiver) correctly handles prototype lookups and getter functions. Using target[prop] directly inside a get trap bypasses this and can cause infinite recursion or incorrect this binding.Basic Example
// โโ Validation proxy โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function createValidated(target, validators) {
return new Proxy(target, {
set(target, prop, value, receiver) {
const validate = validators[prop];
if (validate) {
const error = validate(value);
if (error) throw new TypeError(`${prop}: ${error}`);
}
return Reflect.set(target, prop, value, receiver);
},
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
}
});
}
const user = createValidated({}, {
name: v => typeof v !== 'string' ? 'must be a string' : null,
age: v => (typeof v !== 'number' || v < 0) ? 'must be a non-negative number' : null,
email: v => !v.includes('@') ? 'must be a valid email' : null,
});
user.name = 'Alice'; // OK
user.age = 30; // OK
try {
user.age = -5; // TypeError: age: must be a non-negative number
} catch (e) { console.error(e.message); }
// โโ Logging / tracing proxy โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function createTraced(target, label = 'proxy') {
return new Proxy(target, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (typeof value === 'function') {
return function (...args) {
console.log(`[${label}] ${String(prop)}(${args.map(JSON.stringify).join(', ')})`);
const result = value.apply(this === proxy ? target : this, args);
console.log(`[${label}] ${String(prop)} ->`, result);
return result;
};
}
console.log(`[${label}] get ${String(prop)} =`, value);
return value;
},
set(target, prop, value, receiver) {
console.log(`[${label}] set ${String(prop)} =`, value);
return Reflect.set(target, prop, value, receiver);
}
});
}
const arr = createTraced([], 'arr');
arr.push(1, 2, 3); // [arr] push(1, 2, 3) -> 3
console.log(arr[0]); // [arr] get 0 = 1
// โโ Default values proxy โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function withDefaults(target, defaults) {
return new Proxy(target, {
get(target, prop, receiver) {
const val = Reflect.get(target, prop, receiver);
return val !== undefined ? val : defaults[prop];
}
});
}
const config = withDefaults(
{ theme: 'dark' },
{ theme: 'light', lang: 'en', fontSize: 16, sidebar: true }
);
console.log(config.theme); // 'dark' โ override
console.log(config.lang); // 'en' โ default
console.log(config.fontSize); // 16 โ default
// โโ Revocable proxy โ temporary access โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const { proxy, revoke } = Proxy.revocable({ secret: 42 }, {
get(target, prop) {
console.log(`Accessing ${String(prop)}`);
return Reflect.get(target, prop);
}
});
console.log(proxy.secret); // Accessing secret โ 42
revoke(); // Cut off access
try {
console.log(proxy.secret); // TypeError: Cannot perform 'get' on a proxy that has been revoked
} catch(e) { console.error(e.message); }
// โโ Deep reactive proxy โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function reactive(obj, onChange) {
return new Proxy(obj, {
get(target, prop, receiver) {
const val = Reflect.get(target, prop, receiver);
// Recursively proxy nested objects
return val !== null && typeof val === 'object'
? reactive(val, onChange)
: val;
},
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver);
if (result) onChange(prop, value);
return result;
}
});
}
const state = reactive({ user: { name: 'Alice', age: 30 } }, (prop, val) => {
console.log(`State changed: ${prop} =`, val);
});
state.user.name = 'Bob'; // State changed: name = Bob
state.user.age = 31; // State changed: age = 31
How It Works
Step 1 โ Proxy Wraps a Target with a Handler
new Proxy(target, handler) creates a proxy object. All operations on the proxy are forwarded to the handler object. If a handler method (trap) exists, it intercepts the operation. If not, the operation passes through to the target unchanged. The target and handler are separate objects โ you can use the same handler for multiple targets.
Step 2 โ Reflect Mirrors Every Proxy Trap
For every trap a Proxy can define, there is a corresponding Reflect method that performs the default operation. Reflect.get(target, prop, receiver) does exactly what a get trap would do without interception โ it reads the property from the target, walking the prototype chain and invoking getters with the correct this. Using Reflect inside traps is the correct way to implement “intercept and forward.”
Step 3 โ The receiver Parameter Handles Getters Correctly
When a property access goes through a getter, this inside the getter should be the proxy (or derived object), not the raw target. The receiver parameter captures this. Always pass receiver to Reflect.get() and Reflect.set() โ skipping it breaks inheritance and getters on proxied objects.
Step 4 โ Revocable Proxies Enable Capability Revocation
Proxy.revocable(target, handler) returns { proxy, revoke }. After revoke() is called, any operation on the proxy throws a TypeError. This enables security patterns where you grant temporary, revocable access to an object โ useful for sandboxing, session tokens, and capability-based access control.
Step 5 โ Deep Reactivity Requires Recursive Proxying
A simple proxy on a root object intercepts top-level property access but not nested object mutations. To make nested objects reactive, wrap the returned value of a get trap in another proxy if it is an object. This is the core technique used by Vue 3’s reactivity system โ the entire state tree is lazily wrapped as proxies are accessed.
Real-World Example: Schema Validator Proxy
// schema-validator.js
function createSchema(schema) {
const types = {
string: v => typeof v === 'string',
number: v => typeof v === 'number' && !isNaN(v),
boolean: v => typeof v === 'boolean',
array: v => Array.isArray(v),
object: v => v !== null && typeof v === 'object' && !Array.isArray(v),
};
return function validate(obj) {
return new Proxy(obj, {
set(target, prop, value, receiver) {
const rule = schema[prop];
if (rule) {
if (rule.required && (value === null || value === undefined)) {
throw new TypeError(`${String(prop)}: field is required`);
}
if (rule.type && !types[rule.type]?.(value)) {
throw new TypeError(`${String(prop)}: expected ${rule.type}, got ${typeof value}`);
}
if (rule.min !== undefined && value < rule.min) {
throw new RangeError(`${String(prop)}: must be >= ${rule.min}`);
}
if (rule.max !== undefined && value > rule.max) {
throw new RangeError(`${String(prop)}: must be <= ${rule.max}`);
}
if (rule.pattern && !rule.pattern.test(value)) {
throw new TypeError(`${String(prop)}: does not match required pattern`);
}
if (rule.enum && !rule.enum.includes(value)) {
throw new TypeError(`${String(prop)}: must be one of [${rule.enum.join(', ')}]`);
}
}
return Reflect.set(target, prop, value, receiver);
},
get(target, prop, receiver) {
const rule = schema[prop];
const val = Reflect.get(target, prop, receiver);
if (val === undefined && rule?.default !== undefined) {
return rule.default;
}
return val;
}
});
};
}
const validateUser = createSchema({
name: { type: 'string', required: true, min: 2 },
age: { type: 'number', min: 0, max: 150 },
email: { type: 'string', pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
role: { type: 'string', enum: ['user', 'admin', 'moderator'], default: 'user' },
});
const user = validateUser({});
user.name = 'Alice';
user.age = 30;
user.email = 'alice@example.com';
console.log(user.role); // 'user' โ default value
try { user.age = 200; } // RangeError: age: must be <= 150
catch(e) { console.error(e.message); }
try { user.role = 'superuser'; } // TypeError: role: must be one of [user, admin, moderator]
catch(e) { console.error(e.message); }
Common Mistakes
Mistake 1 โ Not using Reflect in the set trap โ not returning true
โ Wrong โ set trap returns undefined (falsy) in strict mode, causing TypeError:
const p = new Proxy({}, {
set(target, prop, value) {
target[prop] = value;
// Missing return โ returns undefined โ TypeError in strict mode!
}
});
โ Correct โ return Reflect.set:
const p = new Proxy({}, {
set(target, prop, value, receiver) {
target[prop] = value;
return Reflect.set(target, prop, value, receiver); // returns true
}
});
Mistake 2 โ Mutating the target directly inside a get trap โ missing receiver
โ Wrong โ breaks inherited getters:
get(target, prop) {
return target[prop]; // skips prototype chain correctly but wrong this for getters
}
โ Correct โ always use Reflect.get with receiver:
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
}
Mistake 3 โ Proxying hot paths causing performance degradation
โ Wrong โ Proxy on a function called thousands of times per second:
const fn = new Proxy(heavyComputeFn, {
apply(target, thisArg, args) {
return Reflect.apply(target, thisArg, args);
}
});
// Each of 100,000 calls has extra proxy overhead
โ Correct โ use Proxy only for configuration / reactive state, not hot paths:
// Proxy the config object read rarely โ not the compute function called constantly
const config = new Proxy(rawConfig, validationHandler);
Quick Reference
| Task | Code |
|---|---|
| Create proxy | new Proxy(target, { get(t,p,r){}, set(t,p,v,r){} }) |
| Default get | return Reflect.get(target, prop, receiver) |
| Default set | return Reflect.set(target, prop, value, receiver) |
Intercept in |
has(target, prop) { return Reflect.has(target, prop) } |
| Intercept call | apply(target, thisArg, args) { return Reflect.apply(...) } |
| Revocable proxy | const { proxy, revoke } = Proxy.revocable(target, handler) |
| All own keys | Reflect.ownKeys(obj) |