Proxy
This lesson explains Proxy with clear examples, use cases, and best practices so you can use modern JavaScript confidently in real projects.
ES6+ Proxy Overview
Proxy is a built-in ES6 feature that lets you wrap an
object and intercept fundamental operations such as property reads,
writes, deletion, and function calls. You define custom behavior
through a handler object containing trap functions.
Proxy is powerful for validation, logging, access control, reactive
state, and metaprogramming. It is commonly paired with
Reflect, whose methods mirror Proxy trap names and
provide default forwarding behavior.
| Feature | Description |
| Introduced In | ES6 (ECMAScript 2015) |
| Syntax | new Proxy(target, handler) |
| Target | The original object being wrapped |
| Handler | Object with trap functions |
| Common Traps | get, set, has, deleteProperty |
| Typical Uses | Validation, logging, reactive data, API wrappers |
Basic Proxy Example
const user = {
name: "Alex",
role: "admin"
};
const handler = {
get(target, key) {
console.log(
`Reading property: ${String(key)}`
);
return target[key];
},
set(target, key, value) {
console.log(
`Setting ${String(key)} to ${value}`
);
target[key] = value;
return true;
}
};
const proxy =
new Proxy(user, handler);
console.log(proxy.name);
proxy.role = "editor";
A Proxy wraps the original user object. Every property
read and write passes through the handler traps before reaching the
target.
How Proxy Works
When code interacts with a proxy, JavaScript calls the matching
trap in the handler. If no trap exists, the default behavior runs
on the target object.
| Step | Description | Example |
| Create Target | Start with a normal object or function. | const data = { ... } |
| Define Handler | Add trap functions for operations to intercept. | { get() { ... } } |
| Wrap with Proxy | Create the proxy instance. | new Proxy(data, handler) |
| Use Proxy | Access properties through the proxy. | proxy.title |
| Trap Runs | Handler logic executes before target access. | get(target, key) |
Common Proxy Traps
| Trap | Intercepts | Reflect Equivalent |
get | Property read | Reflect.get() |
set | Property assignment | Reflect.set() |
has | in operator | Reflect.has() |
deleteProperty | delete operator | Reflect.deleteProperty() |
ownKeys | Object.keys() and related APIs | Reflect.ownKeys() |
apply | Function call | Reflect.apply() |
construct | new operator | Reflect.construct() |
Validation Proxy Example
function createUser(data) {
return new Proxy(data, {
set(target, key, value) {
if (
key === "age" &&
typeof value !== "number"
) {
throw new TypeError(
"Age must be a number"
);
}
if (
key === "email" &&
!String(value).includes("@")
) {
throw new TypeError(
"Email must be valid"
);
}
return Reflect.set(
target,
key,
value
);
}
});
}
const user =
createUser({
name: "Sam",
age: 25,
email: "sam@example.com"
});
user.age = 26;
console.log(user);
Proxy traps are ideal for validating data before it is stored on an
object. Invalid values can throw errors or be rejected.
Default Values with get Trap
const defaults = {
theme: "light",
language: "en"
};
const settings =
new Proxy(defaults, {
get(target, key) {
if (key in target) {
return Reflect.get(
target,
key
);
}
return undefined;
}
});
console.log(settings.theme);
console.log(settings.language);
console.log(settings.timezone);
The get trap can return fallback values, hide
sensitive fields, or transform data before it reaches the caller.
Controlling Property Visibility
const profile = {
name: "Alex",
email: "alex@example.com",
password: "secret123"
};
const safeProfile =
new Proxy(profile, {
has(target, key) {
if (key === "password") {
return false;
}
return Reflect.has(
target,
key
);
},
get(target, key) {
if (key === "password") {
return undefined;
}
return Reflect.get(
target,
key
);
}
});
console.log(
"password" in safeProfile
);
console.log(safeProfile.name);
Use has and get traps together to hide
internal or sensitive properties from consumers of an object.
Blocking Property Deletion
const config = {
apiUrl: "https://api.example.com",
version: "1.0.0"
};
const lockedConfig =
new Proxy(config, {
deleteProperty(target, key) {
if (key === "apiUrl") {
console.log(
"Cannot delete apiUrl"
);
return false;
}
return Reflect.deleteProperty(
target,
key
);
}
});
delete lockedConfig.version;
delete lockedConfig.apiUrl;
console.log(lockedConfig);
The deleteProperty trap lets you prevent removal of
critical configuration keys while allowing other properties to be
deleted normally.
Proxy for Functions
function add(a, b) {
return a + b;
}
const tracedAdd =
new Proxy(add, {
apply(target, thisArg, args) {
console.log(
"Calling add with",
args
);
return Reflect.apply(
target,
thisArg,
args
);
}
});
console.log(tracedAdd(4, 6));
Functions can also be proxied. The apply trap intercepts
function calls, which is useful for tracing, timing, or middleware
patterns.
Forwarding with Reflect
const product = {
title: "Monitor",
price: 299
};
const handler = {
get(target, key, receiver) {
return Reflect.get(
target,
key,
receiver
);
},
set(target, key, value, receiver) {
return Reflect.set(
target,
key,
value,
receiver
);
}
};
const proxy =
new Proxy(product, handler);
proxy.price = 279;
console.log(proxy.price);
Forwarding traps to Reflect preserves default object
behavior while giving you a clean place to add custom logic around
it.
Revocable Proxy
const target = {
status: "active"
};
const handler = {
get(obj, key) {
return obj[key];
}
};
const { proxy, revoke } =
Proxy.revocable(
target,
handler
);
console.log(proxy.status);
revoke();
try {
console.log(proxy.status);
} catch (error) {
console.log(error.message);
}
Proxy.revocable() creates a proxy that can be
disabled later. After revocation, any operation on the proxy throws
a TypeError.
Simple Reactive State Pattern
function createReactiveState(
initialState,
onChange
) {
return new Proxy(initialState, {
set(target, key, value) {
const previous =
target[key];
const result =
Reflect.set(
target,
key,
value
);
if (previous !== value) {
onChange(key, value, previous);
}
return result;
}
});
}
const state =
createReactiveState(
{ count: 0 },
(key, value) => {
console.log(
`${key} changed to ${value}`
);
}
);
state.count = 1;
state.count = 2;
Proxy is a foundation for reactive systems. The
set trap detects changes and runs callbacks when state
updates.
Common Proxy Use Cases
- Validating object properties before assignment.
- Logging reads and writes for debugging.
- Hiding sensitive or internal fields.
- Building reactive state and data-binding utilities.
- Creating read-only or partially mutable objects.
- Wrapping APIs with access control or rate limiting.
- Implementing virtual or computed properties.
Proxy Best Practices
- Forward unhandled behavior to
Reflect methods. - Always return the correct boolean from
set and deleteProperty traps. - Keep trap logic focused on one responsibility.
- Use Proxy for cross-cutting behavior, not everyday property access.
- Document proxy behavior clearly for other developers.
- Test edge cases such as symbol keys and inherited properties.
- Consider
Proxy.revocable() when access should expire.
Common Proxy Mistakes
- Forgetting to return
true or Reflect.set(...) from a set trap. - Using Proxy where simpler validation functions would be clearer.
- Assuming Proxy intercepts all JavaScript object behavior.
- Mutating the target directly and bypassing the proxy wrapper.
- Not handling symbol property keys in traps.
- Creating deeply nested proxies without a clear purpose.
- Expecting Proxy to work on primitive values without wrapping them in objects.
Proxy vs Reflect
| Feature | Proxy | Reflect |
| Role | Intercepts operations on a target. | Performs default object operations. |
| Syntax | new Proxy(target, handler) | Reflect.get(...) |
| Customization | High — define trap behavior. | Low — standard behavior only. |
| Together | Provides interception layer. | Used to forward default behavior. |
Key Takeaways
- Proxy wraps an object and intercepts operations through handler traps.
- Common traps include
get, set, has, and deleteProperty. - Use Proxy for validation, logging, access control, and reactive patterns.
- Pair Proxy with Reflect to preserve default object behavior.
- Return correct values from traps, especially in
set handlers.
Pro Tip
Start with one trap such as set for validation, forward
everything else to Reflect, and only add more traps when you have a
clear need.