docs/beyond/concepts/proxy-reflect.mdx
What if you could intercept every property access on an object? What if reading user.name could trigger a function, or setting user.age = -5 could throw an error automatically?
const user = { name: 'Alice', age: 30 }
const proxy = new Proxy(user, {
get(target, prop) {
console.log(`Reading ${prop}`)
return target[prop]
},
set(target, prop, value) {
if (prop === 'age' && value < 0) {
throw new Error('Age cannot be negative')
}
target[prop] = value
return true
}
})
proxy.name // Logs: "Reading name", returns "Alice"
proxy.age = -5 // Error: Age cannot be negative
This is the power of Proxy and Reflect. Proxies let you intercept and customize fundamental operations on objects, while Reflect provides the default behavior you can forward to. Together, they enable validation, logging, reactive data binding, and other metaprogramming patterns. According to the ECMAScript specification, Proxy traps map directly to internal methods that define how all JavaScript objects behave at the engine level.
<Info> **What you'll learn in this guide:** - What Proxy is and how it wraps objects to intercept operations - The 13 handler traps (get, set, has, deleteProperty, apply, construct, and more) - Why Reflect exists and how it complements Proxy - Practical patterns: validation, logging, reactive systems, access control - Revocable proxies for temporary access - Limitations and gotchas to avoid </Info> <Warning> **Prerequisites:** This guide builds on [Property Descriptors](/beyond/concepts/property-descriptors) and [Object Methods](/beyond/concepts/object-methods). Understanding how objects work at a lower level helps you see why Proxy is so powerful. </Warning>A Proxy is a wrapper around an object (called the "target") that intercepts operations like reading properties, writing properties, deleting properties, and more. You define custom behavior by providing a "handler" object with "trap" methods.
Think of a Proxy as a security guard standing between you and an object. Every time you try to do something with the object, the guard can inspect, modify, or block the operation.
const target = { message: 'hello' }
const handler = {
get(target, prop) {
return prop in target ? target[prop] : 'Property not found'
}
}
const proxy = new Proxy(target, handler)
console.log(proxy.message) // "hello"
console.log(proxy.missing) // "Property not found"
Without a handler, a Proxy acts as a transparent pass-through:
const target = { x: 10 }
const proxy = new Proxy(target, {}) // Empty handler
proxy.y = 20
console.log(target.y) // 20 - operation forwarded to target
┌─────────────────────────────────────────────────────────────────────────┐
│ PROXY: THE SECURITY GUARD │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ YOUR CODE PROXY TARGET OBJECT │
│ ───────── ───── ───────────── │
│ │
│ ┌────────┐ ┌──────────┐ ┌──────────┐ │
│ │ │ obj.name │ GUARD │ target.name │ { name: │ │
│ │ You │ ──────────► │ │ ─────────────► │ 'Bob' │ │
│ │ │ │ • Check │ │ } │ │
│ │ │ ◄────────── │ • Log │ ◄───────────── │ │ │
│ │ │ "Bob" │ • Modify│ "Bob" │ │ │
│ └────────┘ └──────────┘ └──────────┘ │
│ │
│ The guard can: │
│ • Let the operation through unchanged │
│ • Modify the result before returning it │
│ • Block the operation entirely (throw an error) │
│ • Log the operation for debugging │
│ │
└─────────────────────────────────────────────────────────────────────────┘
A Proxy can intercept 13 different operations. Each trap corresponds to an internal JavaScript operation:
| Trap | Intercepts | Example Operation |
|---|---|---|
get | Reading a property | obj.prop, obj['prop'] |
set | Writing a property | obj.prop = value |
has | The in operator | 'prop' in obj |
deleteProperty | The delete operator | delete obj.prop |
apply | Function calls | func(), func.call() |
construct | The new operator | new Constructor() |
getPrototypeOf | Getting prototype | Object.getPrototypeOf(obj) |
setPrototypeOf | Setting prototype | Object.setPrototypeOf(obj, proto) |
isExtensible | Checking extensibility | Object.isExtensible(obj) |
preventExtensions | Preventing extensions | Object.preventExtensions(obj) |
getOwnPropertyDescriptor | Getting descriptor | Object.getOwnPropertyDescriptor(obj, prop) |
defineProperty | Defining property | Object.defineProperty(obj, prop, desc) |
ownKeys | Listing own keys | Object.keys(obj), for...in |
Let's explore the most commonly used traps in detail.
get Trap: Intercepting Property AccessThe get trap fires whenever you read a property:
const handler = {
get(target, prop, receiver) {
console.log(`Accessing: ${prop}`)
return target[prop]
}
}
const user = new Proxy({ name: 'Alice' }, handler)
console.log(user.name) // Logs: "Accessing: name", returns "Alice"
Parameters:
target - The original objectprop - The property name (string or Symbol)receiver - The proxy itself (or an object inheriting from it)Return a default value for missing properties:
const defaults = new Proxy({}, {
get(target, prop) {
return prop in target ? target[prop] : 0
}
})
defaults.x = 10
console.log(defaults.x) // 10
console.log(defaults.missing) // 0 (not undefined!)
Access array elements from the end with negative indices:
function createNegativeArray(arr) {
return new Proxy(arr, {
get(target, prop, receiver) {
const index = Number(prop)
if (index < 0) {
return target[target.length + index]
}
return Reflect.get(target, prop, receiver)
}
})
}
const arr = createNegativeArray([1, 2, 3, 4, 5])
console.log(arr[-1]) // 5 (last element)
console.log(arr[-2]) // 4 (second to last)
set Trap: Intercepting Property AssignmentThe set trap fires when you assign a value to a property:
const handler = {
set(target, prop, value, receiver) {
console.log(`Setting ${prop} to ${value}`)
target[prop] = value
return true // Must return true for success
}
}
const obj = new Proxy({}, handler)
obj.x = 10 // Logs: "Setting x to 10"
Validate data before allowing assignment:
const validator = {
set(target, prop, value) {
if (prop === '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')
}
}
target[prop] = value
return true
}
}
const person = new Proxy({}, validator)
person.name = 'Alice' // Works fine
person.age = 30 // Works fine
person.age = -5 // RangeError: Age must be between 0 and 150
person.age = 'thirty' // TypeError: Age must be a number
has Trap: Intercepting in OperatorThe has trap intercepts the in operator:
const range = new Proxy({ start: 1, end: 10 }, {
has(target, prop) {
const num = Number(prop)
return num >= target.start && num <= target.end
}
})
console.log(5 in range) // true
console.log(15 in range) // false
console.log(1 in range) // true
deleteProperty TrapIntercept property deletion:
const protected = new Proxy({ id: 1, name: 'Alice' }, {
deleteProperty(target, prop) {
if (prop === 'id') {
throw new Error('Cannot delete id property')
}
delete target[prop]
return true
}
})
delete protected.name // Works
delete protected.id // Error: Cannot delete id property
apply and construct TrapsFor function proxies, you can intercept calls and new invocations:
function sum(a, b) {
return a + b
}
const loggedSum = new Proxy(sum, {
apply(target, thisArg, args) {
console.log(`Called with: ${args}`)
return target.apply(thisArg, args)
}
})
loggedSum(1, 2) // Logs: "Called with: 1,2", returns 3
The construct trap intercepts new:
class User {
constructor(name) {
this.name = name
}
}
const TrackedUser = new Proxy(User, {
construct(target, args) {
console.log(`Creating user: ${args[0]}`)
return new target(...args)
}
})
const user = new TrackedUser('Alice') // Logs: "Creating user: Alice"
ownKeys Trap: Filtering PropertiesThe ownKeys trap intercepts operations that list object keys:
const user = {
name: 'Alice',
age: 30,
_password: 'secret123'
}
const safeUser = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'))
}
})
console.log(Object.keys(safeUser)) // ["name", "age"] - _password hidden
Reflect is a built-in object with methods that mirror every Proxy trap. It provides the default behavior you'd otherwise have to implement manually. MDN documents that Reflect was introduced alongside Proxy in ES2015 specifically to provide a clean, function-based API for object operations that previously required operators or Object.* methods.
| Operation | Without Reflect | With Reflect |
|---|---|---|
| Read property | target[prop] | Reflect.get(target, prop, receiver) |
| Write property | target[prop] = value | Reflect.set(target, prop, value, receiver) |
| Delete property | delete target[prop] | Reflect.deleteProperty(target, prop) |
| Check property | prop in target | Reflect.has(target, prop) |
Reflect.set returns true/false instead of the assigned valueconst handler = {
get(target, prop, receiver) {
console.log(`Reading ${prop}`)
return Reflect.get(target, prop, receiver) // Proper forwarding
},
set(target, prop, value, receiver) {
console.log(`Writing ${prop}`)
return Reflect.set(target, prop, value, receiver) // Returns boolean
}
}
The receiver parameter is crucial when the target has getters:
const user = {
_name: 'Alice',
get name() {
return this._name
}
}
const proxy = new Proxy(user, {
get(target, prop, receiver) {
// ❌ WRONG - 'this' will be target, not proxy
// return target[prop]
// ✓ CORRECT - 'this' will be receiver (the proxy)
return Reflect.get(target, prop, receiver)
}
})
Create objects that notify you when they change. This is how frameworks like Vue.js implement reactivity. According to the Vue.js documentation, Vue 3 replaced Object.defineProperty() (used in Vue 2) with Proxy for its reactivity system, enabling detection of property additions and deletions that were previously impossible:
function observable(target, onChange) {
return new Proxy(target, {
set(target, prop, value, receiver) {
const oldValue = target[prop]
const result = Reflect.set(target, prop, value, receiver)
if (result && oldValue !== value) {
onChange(prop, oldValue, value)
}
return result
}
})
}
const state = observable({ count: 0 }, (prop, oldVal, newVal) => {
console.log(`${prop} changed from ${oldVal} to ${newVal}`)
})
state.count = 1 // Logs: "count changed from 0 to 1"
state.count = 2 // Logs: "count changed from 1 to 2"
Hide private properties (those starting with _):
const privateHandler = {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error(`Access denied: ${prop} is private`)
}
return Reflect.get(...arguments)
},
set(target, prop, value) {
if (prop.startsWith('_')) {
throw new Error(`Access denied: ${prop} is private`)
}
return Reflect.set(...arguments)
},
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'))
}
}
const user = new Proxy({ name: 'Alice', _password: 'secret' }, privateHandler)
console.log(user.name) // "Alice"
console.log(Object.keys(user)) // ["name"] - _password hidden
console.log(user._password) // Error: Access denied
Log all operations on an object:
function createLogged(target, name = 'Object') {
return new Proxy(target, {
get(target, prop, receiver) {
console.log(`[${name}] GET ${String(prop)}`)
return Reflect.get(target, prop, receiver)
},
set(target, prop, value, receiver) {
console.log(`[${name}] SET ${String(prop)} = ${value}`)
return Reflect.set(target, prop, value, receiver)
}
})
}
const user = createLogged({ name: 'Alice' }, 'User')
user.name // [User] GET name
user.age = 30 // [User] SET age = 30
Sometimes you need to grant temporary access to an object. Proxy.revocable() creates a proxy that can be disabled:
const target = { secret: 'classified info' }
const { proxy, revoke } = Proxy.revocable(target, {})
console.log(proxy.secret) // "classified info"
revoke() // Disable the proxy
console.log(proxy.secret) // TypeError: Cannot perform 'get' on a proxy that has been revoked
This is useful for:
Some built-in objects like Map, Set, Date, and Promise use internal slots that Proxy can't intercept:
const map = new Map()
const proxy = new Proxy(map, {})
proxy.set('key', 'value') // TypeError: Method Map.prototype.set called on incompatible receiver
Workaround: Bind methods to the target:
const map = new Map()
const proxy = new Proxy(map, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver)
return typeof value === 'function' ? value.bind(target) : value
}
})
proxy.set('key', 'value') // Works!
console.log(proxy.get('key')) // "value"
Private fields (#field) also use internal slots and don't work through proxies:
class Secret {
#hidden = 'secret'
reveal() {
return this.#hidden
}
}
const secret = new Secret()
const proxy = new Proxy(secret, {})
proxy.reveal() // TypeError: Cannot read private member
A proxy is a different object from its target:
const target = {}
const proxy = new Proxy(target, {})
console.log(proxy === target) // false
const set = new Set([target])
console.log(set.has(proxy)) // false - they're different objects
Proxy wraps objects to intercept operations like property access, assignment, deletion, and function calls.
Handlers define traps that are methods named after the operations they intercept (get, set, has, deleteProperty, etc.).
There are 13 traps covering all fundamental object operations, from property access to prototype manipulation.
The set trap must return true for successful writes, or you'll get a TypeError in strict mode.
Reflect provides default behavior with the same method names as Proxy traps, making forwarding clean and correct.
Use Reflect.get/set with receiver to properly handle getters/setters in inheritance chains.
Revocable proxies can be disabled with revoke(), useful for temporary access patterns.
Built-in objects with internal slots (Map, Set, Date) need the method-binding workaround.
Private class fields don't work through proxies due to internal slot access.
Proxies enable powerful patterns like validation, observable data, access control, and debugging.
</Info>In strict mode, returning `false` from a `set` trap causes a `TypeError`. In non-strict mode, the assignment silently fails.
```javascript
'use strict'
const proxy = new Proxy({}, {
set() {
return false // Or return nothing (undefined)
}
})
proxy.x = 10 // TypeError: 'set' on proxy returned false
```
Always return `true` from `set` traps when the operation should succeed.
`Reflect.get(target, prop, receiver)` properly forwards the `receiver`, which is essential when the target has getters that use `this`:
```javascript
const user = {
firstName: 'Alice',
lastName: 'Smith',
get fullName() {
return `${this.firstName} ${this.lastName}`
}
}
const proxy = new Proxy(user, {
get(target, prop, receiver) {
// With target[prop], 'this' in the getter would be 'target'
// With Reflect.get, 'this' in the getter is 'receiver' (the proxy)
return Reflect.get(target, prop, receiver)
}
})
```
This matters when you proxy an object that inherits from another proxy.
Built-in objects like Map and Set use internal slots that proxies can't access. The workaround is to bind methods to the original target:
```javascript
const map = new Map()
const proxy = new Proxy(map, {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver)
// If it's a function, bind it to the target
return typeof value === 'function' ? value.bind(target) : value
}
})
proxy.set('key', 'value') // Works now!
```
`Object.defineProperty` only validates a single, predefined property. Proxy intercepts all operations dynamically:
```javascript
// defineProperty: Must define each property in advance
const user = {}
Object.defineProperty(user, 'age', {
set(value) {
if (value < 0) throw new Error('Invalid age')
this._age = value
}
})
// Proxy: Works for any property, including new ones
const user2 = new Proxy({}, {
set(target, prop, value) {
if (prop === 'age' && value < 0) {
throw new Error('Invalid age')
}
return Reflect.set(...arguments)
}
})
```
Proxy is more flexible for dynamic validation rules.
Use `Proxy.revocable()` instead of `new Proxy()`:
```javascript
const { proxy, revoke } = Proxy.revocable({ data: 'sensitive' }, {})
console.log(proxy.data) // "sensitive"
revoke() // Disable the proxy permanently
console.log(proxy.data) // TypeError: proxy has been revoked
```
Once revoked, the proxy cannot be re-enabled. All operations on it throw TypeError.