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

  1. What Is a Proxy?
  2. Basic Syntax
  3. Intercepting Property Access — get Trap
  4. Validating Writes — set Trap
  5. Default Values for Missing Properties
  6. Logging Every Change
  7. Access Control
  8. Intercepting Function Calls — apply Trap
  9. Common Proxy Traps
  10. Real-World Usage
  11. When to Use and When to Avoid
  12. The Proxy Pattern Beyond JavaScript
  13. Summary

What Is a Proxy?

↑ Index

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

↑ Index

const proxy = new Proxy(target, handler);
ParameterDescription
targetThe original object you want to wrap
handlerAn 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

↑ Index

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:

  1. proxyUser.name is accessed
  2. The proxy intercepts it with the get trap
  3. Logs the access
  4. 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

↑ Index

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:

ArgumentDescription
targetThe original object
propertyThe property name being set
valueThe new value being assigned

Returning true signals that the assignment succeeded. Throwing an error prevents the assignment entirely.


Default Values for Missing Properties

↑ Index

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

↑ Index

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

↑ Index

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

↑ Index

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:

ArgumentDescription
targetThe original function
thisArgThe this value for the call
argsAn array of arguments passed to the function

This is useful for logging, measuring performance, or adding validation before a function executes.


Common Proxy Traps

↑ Index

TrapTriggered when
getReading a property
setWriting a property
hasUsing the in operator
deletePropertyDeleting a property
applyCalling a function
constructUsing new
ownKeysListing 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

↑ Index

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

↑ Index

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

↑ Index

The proxy pattern — an intermediary that intercepts operations before they reach the real target — appears far beyond JavaScript's new Proxy():

ContextWhat it interceptsExample
JavaScript new Proxy()Object/function operations (get, set, delete)Validation, logging, reactivity
Next.js proxy.tsHTTP requests (before routes render)Authentication, redirects, headers
Network proxiesNetwork traffic between client and serverLoad balancers, CDNs, reverse proxies
Design patternsMethod calls on an objectLazy 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.ts file convention to proxy.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's proxy.ts does not use JavaScript's new Proxy() internally. They share the concept, not the implementation. See the Next.js tutorial on Middleware to Proxy migration for details.


Summary

↑ Index

ConceptDetails
new Proxy(target, handler)Creates a wrapper that intercepts operations on the target
targetThe original object or function being wrapped
handlerAn object containing traps (interceptor functions)
get trapIntercepts property reads — use for logging, defaults, access control
set trapIntercepts property writes — use for validation, logging, reactivity
apply trapIntercepts function calls — use for logging, performance, validation
Mental modelCaller → Proxy → Real Object — the proxy decides what happens first
Real-worldVue 3 reactivity, validation layers, debugging, framework internals
PerformanceProxies add overhead — avoid in tight loops or simple cases