Advanced JavaScript Proxies: Complete Guide
The Proxy object lets you intercept and redefine fundamental operations on any target object. Learn every trap, revocation, and real-world use case for JavaScript proxies.
A JavaScript Proxy wraps a target object and sits between it and every piece of code that interacts with it. You define a handler that intercepts specific operations, called traps, and decide what happens when someone reads a property, writes a value, deletes a key, or even calls the object as a function.
const target = { message: "hello" };
const handler = {
get(obj, prop) {
console.log(`Reading property "${prop}"`);
return obj[prop];
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.message);
// Reading property "message"
// helloThe get trap fires before the value is returned. You can transform it, log it, block it, or return a computed value instead. The original target object never knows it has been wrapped.
How Proxies Intercept Operations
Every interaction with an object ultimately calls an internal method defined in the ECMAScript specification. Proxy handlers override those internal methods:
If the handler defines a trap for the operation, the trap runs and its return value is used. If no trap is defined, the operation falls through to the target object as if the proxy were not there.
The 13 Proxy Traps
The full Proxy API defines traps for every fundamental object operation. Here are all 13, organized by category:
Property Access Traps
| Trap | Intercepts | Signature |
|---|---|---|
get | Reading a property | (target, prop, receiver) |
set | Writing a property | (target, prop, value, receiver) |
has | The in operator | (target, prop) |
deleteProperty | The delete operator | (target, prop) |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor | (target, prop) |
defineProperty | Object.defineProperty | (target, prop, descriptor) |
ownKeys | Object.keys, Object.getOwnPropertyNames, for...in | (target) |
Function and Construction Traps
| Trap | Intercepts | Signature |
|---|---|---|
apply | Calling the proxy as a function | (target, thisArg, argumentsList) |
construct | Using new on the proxy | (target, argumentsList, newTarget) |
Prototype and Extensibility Traps
| Trap | Intercepts | Signature |
|---|---|---|
getPrototypeOf | Object.getPrototypeOf | (target) |
setPrototypeOf | Object.setPrototypeOf | (target, proto) |
isExtensible | Object.isExtensible | (target) |
preventExtensions | Object.preventExtensions | (target) |
Most real-world proxy use cases only need get, set, has, and deleteProperty, plus occasionally apply or construct. The other traps are for specialized scenarios like virtual object hierarchies or sandboxing.
The Reflect API -- Your Default Companion
Inside every trap, you decide whether to forward the operation to the target or return something else entirely. The Reflect API provides the default behavior for every trap as a simple function call:
const handler = {
get(target, prop, receiver) {
if (prop.startsWith("_")) {
throw new Error(`Cannot access private property "${prop}"`);
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
if (prop.startsWith("_")) {
throw new Error(`Cannot modify private property "${prop}"`);
}
return Reflect.set(target, prop, value, receiver);
}
};
const user = new Proxy({ name: "Alex", _password: "secret" }, handler);
console.log(user.name); // "Alex"
console.log(user._password); // Error: Cannot access private property "_password"The pattern is always the same: check a condition, then either return your custom logic or call the matching Reflect method. Reflect.get and Reflect.set handle the prototype chain and accessor properties correctly, so you do not need to reimplement those details.
Practical Use Case: Validation Proxy
One of the most common proxy patterns is validating property assignments before they reach the target:
function createValidatedUser(initialData) {
const target = { ...initialData };
const handler = {
set(obj, prop, value) {
if (prop === "age") {
if (typeof value !== "number" || value < 0 || value > 150) {
throw new TypeError(`Invalid age: ${value}`);
}
}
if (prop === "email") {
if (!value.includes("@")) {
throw new TypeError(`Invalid email: ${value}`);
}
}
obj[prop] = value;
return true;
}
};
return new Proxy(target, handler);
}The set trap checks the property name before writing. An invalid age or email throws a TypeError instead of silently storing bad data:
const user = createValidatedUser({ name: "Sam", age: 28, email: "sam@example.com" });
user.age = 30; // OK
user.age = -5; // TypeError: Invalid age: -5
user.email = "not-valid"; // TypeError: Invalid email: not-validEvery assignment passes through the set trap, which validates before writing. The target object stays clean because no incomplete or invalid data can reach it. This pattern is especially useful for form state, API request bodies, and configuration objects.
Practical Use Case: Logging and Debugging
A proxy can log every interaction with an object without changing any of the code that uses it:
function createLoggingProxy(obj, label = "Object") {
return new Proxy(obj, {
get(target, prop) {
const value = Reflect.get(target, prop);
console.log(`${label}: GET "${String(prop)}" →`, value);
return value;
},
set(target, prop, value) {
console.log(`${label}: SET "${String(prop)}" =`, value);
return Reflect.set(target, prop, value);
},
deleteProperty(target, prop) {
console.log(`${label}: DELETE "${String(prop)}"`);
return Reflect.deleteProperty(target, prop);
}
});
}
const config = createLoggingProxy({ debug: false, theme: "dark" }, "Config");
config.theme = "light"; // Config: SET "theme" = light
console.log(config.debug); // Config: GET "debug" → false
delete config.debug; // Config: DELETE "debug"Add this wrapper during development to trace which code is reading or writing which properties, then remove it in production with zero changes to the rest of your application.
Revocable Proxies
A revocable proxy can be cut off from its target permanently. Once revoked, any operation on the proxy throws a TypeError:
const { proxy, revoke } = Proxy.revocable(
{ data: "sensitive" },
{}
);
console.log(proxy.data); // "sensitive"
revoke();
console.log(proxy.data); // TypeError: Cannot perform 'get' on a proxy
// that has been revokedRevocable proxies are ideal for resource lifecycle management. Give a consumer access to an object, and when they are done, revoke the proxy. Any code that holds a reference to the proxy can no longer reach the target.
This pattern appears in security-sensitive APIs where you want to hand out temporary access and guarantee it cannot be used after a certain point. There is no way to un-revoke a proxy.
Practical Use Case: Default Values
A proxy can supply default values for missing properties instead of returning undefined:
function withDefaults(obj, defaultValue) {
return new Proxy(obj, {
get(target, prop) {
return prop in target ? target[prop] : defaultValue;
}
});
}
const settings = withDefaults({ theme: "dark" }, "not set");
console.log(settings.theme); // "dark"
console.log(settings.language); // "not set"The in operator checks whether the property exists on the target, including its prototype chain. If it does not, the proxy returns the fallback value instead of undefined. This is simpler and more readable than sprinkling a default value check throughout your code.
Practical Use Case: Negative Array Indices
A proxy can make arrays support Python-style negative indexing, where an index of -1 means the last element:
function createArray(...items) {
return new Proxy(items, {
get(target, prop) {
const index = Number(prop);
if (!isNaN(index) && index < 0) {
return target[target.length + index];
}
return Reflect.get(target, prop);
}
});
}
const arr = createArray("a", "b", "c", "d");
console.log(arr[-1]); // "d"
console.log(arr[-2]); // "c"
console.log(arr[0]); // "a"The trap converts negative numeric indices by adding them to the array length before forwarding to the target. Non-numeric properties and positive indices pass through via Reflect.get.
Combining Traps for a Rich Wrapper
Real proxy patterns often combine multiple traps. Here is a proxy that provides both default values and read-only protection for specific properties:
function createReadOnlyWithDefaults(obj, readOnlyKeys, defaultValue) {
return new Proxy(obj, {
get(target, prop) {
return prop in target ? target[prop] : defaultValue;
},
set(target, prop, value) {
if (readOnlyKeys.includes(prop)) {
throw new Error(`Cannot modify read-only property "${String(prop)}"`);
}
return Reflect.set(target, prop, value);
},
deleteProperty(target, prop) {
if (readOnlyKeys.includes(prop)) {
throw new Error(`Cannot delete read-only property "${String(prop)}"`);
}
return Reflect.deleteProperty(target, prop);
}
});
}The get trap fills in missing properties, while set and deleteProperty block writes to the keys listed in readOnlyKeys. Calling the function below wires all three traps onto the same target object:
const user = createReadOnlyWithDefaults(
{ id: 1, name: "Alex" },
["id"],
"N/A"
);
console.log(user.id); // 1
console.log(user.email); // "N/A" (default)
user.name = "Bob"; // OK
user.id = 2; // Error: Cannot modify read-only property "id"This pattern uses three traps working together to create a consistent interface. The target object remains a plain object, so any code can still access it directly if needed. The proxy is an additional access layer, not a replacement for the target.
Proxy Limitations
Proxies are powerful but not transparent in all situations:
- Equality:
proxy === targetis always false. The proxy is a distinct object. - Built-in internal slots: Proxying Map, Set, Date, and Promise instances requires extra care because internal methods may not forward through a plain target. Bind methods explicitly to the target when wrapping built-ins.
- No trap for strict equality or typeof: You cannot intercept the equality or typeof operators.
- Performance: Proxies add measurable overhead. Do not wrap objects accessed in tight loops or rendering hot paths.
For the proxy design pattern (virtual proxy, caching proxy, lazy initialization), see /javascript/the-javascript-proxy-pattern-complete-guide. For reactive state powered by Proxy, see /javascript/building-a-reactive-ui-with-the-js-observer.
Rune AI
Key Insights
- Proxy wraps a target object and intercepts operations through handler traps.
- There are 13 traps covering get, set, delete, function calls, construction, and more.
- Always use Reflect inside traps to forward default behavior where needed.
- Revocable proxies let you cut off access to an object entirely after a certain point.
- Proxy powers Vue 3 reactivity, ORM lazy loading, and API mocking libraries.
Frequently Asked Questions
What is the difference between Proxy and Object.defineProperty?
Does Proxy affect performance?
Conclusion
The Proxy API is JavaScript's most powerful metaprogramming tool. It lets you stand between any object and the code that uses it, intercepting every fundamental operation. Whether you are building validation layers, reactive state systems, API wrappers, or access control, Proxy gives you a structured, safe way to modify behavior without rewriting source code.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.