JavaScript Proxy: Intercept and Control Object Operations
Overview
JavaScript has a built-in Proxy object that lets you create a wrapper around any object or function. This wrapper intercepts and customizes operations performed on the target — reading properties, writing values, deleting keys, calling functions, and more.
Caller → Proxy → Real Object
The proxy decides what happens before the real object is touched.
Table of Contents
- What Is a Proxy?
- Basic Syntax
- Intercepting Property Access —
getTrap - Validating Writes —
setTrap - Default Values for Missing Properties
- Logging Every Change
- Access Control
- Intercepting Function Calls —
applyTrap - Common Proxy Traps
- Real-World Usage
- When to Use and When to Avoid
- The Proxy Pattern Beyond JavaScript
- Summary
What Is a Proxy?
new Proxy() creates an object that intercepts and customizes operations performed on another object or function.
In simple terms: a Proxy lets you control what happens when someone reads, writes, deletes, or calls something on an object.
It is commonly used for:
- Validation — enforce rules before values are set
- Logging — track every property access or change
- Access control — hide or protect sensitive properties
- Reactivity systems — automatically update the UI when data changes (Vue.js)
- Meta-programming — customize fundamental object behavior
Basic Syntax
const proxy = new Proxy(target, handler);
| Parameter | Description |
|---|---|
target | The original object you want to wrap |
handler | An object that defines traps — functions that intercept specific operations |
const target = {};
const handler = {};
const proxy = new Proxy(target, handler);
With an empty handler, the proxy behaves identically to the target — all operations pass through unchanged. Traps are what make it useful.
Intercepting Property Access — get Trap
The get trap runs every time a property is read on the proxy.
const user = { name: "John" };
const handler = {
get(target, property) {
console.log(`Accessing ${property}`);
return target[property];
},
};
const proxyUser = new Proxy(user, handler);
console.log(proxyUser.name);
Output:
Accessing name
John
What happened:
proxyUser.nameis accessed- The proxy intercepts it with the
gettrap - Logs the access
- Returns the original value from the target
The caller interacts with proxyUser as if it were the real user object — but every read passes through the get trap first.
Validating Writes — set Trap
The set trap runs every time a property is written. You can enforce rules before the value reaches the real object.
const person = { age: 20 };
const proxy = new Proxy(person, {
set(target, property, value) {
if (property === "age" && value < 0) {
throw new Error("Age cannot be negative");
}
target[property] = value;
return true;
},
});
proxy.age = 25; // ✅ works
proxy.age = -5; // ❌ throws Error: "Age cannot be negative"
The set trap receives three arguments:
| Argument | Description |
|---|---|
target | The original object |
property | The property name being set |
value | The new value being assigned |
Returning true signals that the assignment succeeded. Throwing an error prevents the assignment entirely.
Default Values for Missing Properties
You can use the get trap to return default values instead of undefined when a property doesn't exist.
const handler = {
get(target, prop) {
if (prop in target) {
return target[prop];
}
return "Not found";
},
};
const proxy = new Proxy({}, handler);
console.log(proxy.name); // "Not found"
console.log(proxy.age); // "Not found"
Instead of getting undefined for missing properties, the proxy returns "Not found".
Logging Every Change
The set trap makes it easy to log every property change — useful for debugging or building audit trails.
const handler = {
set(target, prop, value) {
console.log(`Property ${prop} changed to ${value}`);
target[prop] = value;
return true;
},
};
const state = new Proxy({}, handler);
state.count = 1;
state.count = 2;
Output:
Property count changed to 1
Property count changed to 2
Every assignment is logged automatically — the calling code doesn't need to know it's being observed.
Access Control
You can use a proxy to protect sensitive properties — blocking access to certain fields while allowing others.
const user = { name: "Admin", password: "secret123" };
const proxy = new Proxy(user, {
get(target, prop) {
if (prop === "password") {
return "Access denied";
}
return target[prop];
},
});
console.log(proxy.password); // "Access denied"
console.log(proxy.name); // "Admin"
The real password value is never exposed. The proxy intercepts the read and returns a safe value instead.
Intercepting Function Calls — apply Trap
Proxies can wrap functions too, not just objects. The apply trap intercepts function calls.
function sum(a, b) {
return a + b;
}
const proxy = new Proxy(sum, {
apply(target, thisArg, args) {
console.log("Function called with", args);
return target(...args);
},
});
proxy(2, 3);
Output:
Function called with [2, 3]
5
The apply trap receives:
| Argument | Description |
|---|---|
target | The original function |
thisArg | The this value for the call |
args | An array of arguments passed to the function |
This is useful for logging, measuring performance, or adding validation before a function executes.
Common Proxy Traps
| Trap | Triggered when |
|---|---|
get | Reading a property |
set | Writing a property |
has | Using the in operator |
deleteProperty | Deleting a property |
apply | Calling a function |
construct | Using new |
ownKeys | Listing keys (Object.keys) |
You can define any combination of traps in the handler. Operations without a corresponding trap pass through to the target unchanged.
Real-World Usage
Vue 3 Reactivity
JavaScript's Proxy is the foundation of Vue 3's reactivity system. When you write:
const state = reactive({ count: 0 });
Vue internally creates something similar to:
new Proxy(state, {
get(target, prop) {
// track which component uses this property
track(target, prop);
return target[prop];
},
set(target, prop, value) {
target[prop] = value;
// notify components that use this property
trigger(target, prop);
return true;
},
});
When a component reads state.count, Vue tracks the dependency. When state.count changes, Vue knows exactly which components to re-render. This entire system is powered by Proxy traps.
When to Use and When to Avoid
Use Proxy for:
- Framework internals (reactivity, dependency tracking)
- Validation layers (enforce data rules automatically)
- Debugging and logging (observe all reads/writes)
- Access control (hide or protect properties)
- Security wrappers
Avoid Proxy for:
- Simple objects where direct access is sufficient — Proxy adds overhead
- Performance-critical tight loops — each trapped operation has a function call cost
- Cases where TypeScript types or simple getter/setter methods achieve the same goal
Rule of thumb: If you're building a framework, library, or need to observe object behavior transparently, Proxy is the right tool. For everyday application code, simpler alternatives usually exist.
The Proxy Pattern Beyond JavaScript
The proxy pattern — an intermediary that intercepts operations before they reach the real target — appears far beyond JavaScript's new Proxy():
| Context | What it intercepts | Example |
|---|---|---|
JavaScript new Proxy() | Object/function operations (get, set, delete) | Validation, logging, reactivity |
Next.js proxy.ts | HTTP requests (before routes render) | Authentication, redirects, headers |
| Network proxies | Network traffic between client and server | Load balancers, CDNs, reverse proxies |
| Design patterns | Method calls on an object | Lazy loading, caching, access control |
The core idea is always the same:
Caller → Proxy → Real Target
The proxy decides what happens before the real target is touched.
Note: Next.js 16 renamed its
middleware.tsfile convention toproxy.ts. The name was chosen because the behavior is analogous to a proxy — it sits between the browser and the application, intercepting HTTP requests. However, Next.js'sproxy.tsdoes not use JavaScript'snew Proxy()internally. They share the concept, not the implementation. See the Next.js tutorial on Middleware to Proxy migration for details.
Summary
| Concept | Details |
|---|---|
new Proxy(target, handler) | Creates a wrapper that intercepts operations on the target |
target | The original object or function being wrapped |
handler | An object containing traps (interceptor functions) |
get trap | Intercepts property reads — use for logging, defaults, access control |
set trap | Intercepts property writes — use for validation, logging, reactivity |
apply trap | Intercepts function calls — use for logging, performance, validation |
| Mental model | Caller → Proxy → Real Object — the proxy decides what happens first |
| Real-world | Vue 3 reactivity, validation layers, debugging, framework internals |
| Performance | Proxies add overhead — avoid in tight loops or simple cases |