DocsHub
Advanced

Proxy and Reflect

Learn how to intercept and customize JavaScript object operations using Proxy and Reflect.

Proxy and Reflect

Proxy lets you intercept and customize fundamental operations on objects — reading properties, writing properties, calling functions, checking if a property exists, and more. It is a way to wrap an object and put your own code in between.

Reflect is a companion API — it provides the same operations as the proxy traps but in a clean, functional form. It makes it easy to forward operations from a proxy to the original object.


What is a Proxy?

A Proxy wraps an object and lets you intercept operations on it.

const target = { name: "Ali", age: 22 };

const proxy = new Proxy(target, {
  get(target, property) {
    console.log(`Getting: ${property}`);
    return target[property];
  }
});

console.log(proxy.name);
// Getting: name
// Ali

console.log(proxy.age);
// Getting: age
// 22

target is the original object. The second argument is the handler — an object containing traps — functions that intercept specific operations.


Syntax

const proxy = new Proxy(target, handler);
  • target — the object being wrapped
  • handler — an object with trap methods
  • Each trap corresponds to one JavaScript operation

The Most Important Traps

TrapIntercepts
get(target, prop)Reading a property
set(target, prop, value)Writing a property
has(target, prop)in operator
deleteProperty(target, prop)delete operator
apply(target, thisArg, args)Function calls
construct(target, args)new operator
ownKeys(target)Object.keys(), for...in

get Trap — Intercept Property Reads

const user = { name: "Ali", age: 22 };

const proxy = new Proxy(user, {
  get(target, property) {
    if (property in target) {
      return target[property];
    }
    return `Property "${property}" does not exist`;
  }
});

console.log(proxy.name);        // Ali
console.log(proxy.age);         // 22
console.log(proxy.email);       // Property "email" does not exist

Default values for missing properties

function withDefaults(obj, defaults) {
  return new Proxy(obj, {
    get(target, prop) {
      return prop in target ? target[prop] : defaults[prop];
    }
  });
}

const config = withDefaults(
  { theme: "dark" },
  { theme: "light", language: "en", fontSize: 14, debug: false }
);

console.log(config.theme);    // dark — from target
console.log(config.language); // en — from defaults
console.log(config.fontSize); // 14 — from defaults
console.log(config.debug);    // false — from defaults

set Trap — Intercept Property Writes

function createValidatedObject(schema) {
  return new Proxy({}, {
    set(target, prop, value) {
      if (schema[prop]) {
        const { type, min, max, required } = schema[prop];

        if (type && typeof value !== type) {
          throw new TypeError(`${prop} must be of type ${type}`);
        }

        if (typeof value === "number") {
          if (min !== undefined && value < min) {
            throw new RangeError(`${prop} must be at least ${min}`);
          }
          if (max !== undefined && value > max) {
            throw new RangeError(`${prop} must be at most ${max}`);
          }
        }

        if (typeof value === "string" && min !== undefined && value.length < min) {
          throw new RangeError(`${prop} must be at least ${min} characters`);
        }
      }

      target[prop] = value;
      return true; // must return true to indicate success
    }
  });
}

const user = createValidatedObject({
  name: { type: "string", min: 2 },
  age: { type: "number", min: 0, max: 150 },
  email: { type: "string" }
});

user.name = "Ali";   // ✅
user.age = 22;       // ✅
user.name = "A";     // ❌ RangeError: name must be at least 2 characters
user.age = -5;       // ❌ RangeError: age must be at least 0
user.age = "old";    // ❌ TypeError: age must be of type number

The set trap must return true to indicate the assignment succeeded. If it returns false (or nothing), JavaScript throws a TypeError in strict mode.


has Trap — Intercept the in Operator

const range = new Proxy({ min: 1, max: 100 }, {
  has(target, prop) {
    const num = Number(prop);
    return num >= target.min && num <= target.max;
  }
});

console.log(50 in range);  // true
console.log(1 in range);   // true
console.log(100 in range); // true
console.log(0 in range);   // false
console.log(101 in range); // false

deleteProperty Trap — Intercept delete

function createProtectedObject(obj, protectedKeys) {
  return new Proxy(obj, {
    deleteProperty(target, prop) {
      if (protectedKeys.includes(prop)) {
        throw new Error(`Cannot delete protected property: ${prop}`);
      }
      delete target[prop];
      return true;
    }
  });
}

const config = createProtectedObject(
  { apiUrl: "https://api.example.com", debug: true, version: "1.0" },
  ["apiUrl", "version"]
);

delete config.debug;   // ✅ allowed
delete config.apiUrl;  // ❌ Error: Cannot delete protected property: apiUrl

apply Trap — Intercept Function Calls

The apply trap intercepts calls to a function proxy.

function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

const loggedSum = new Proxy(sum, {
  apply(target, thisArg, args) {
    console.log(`Calling sum with args: [${args}]`);
    const result = target.apply(thisArg, args);
    console.log(`Result: ${result}`);
    return result;
  }
});

loggedSum(1, 2, 3);
// Calling sum with args: [1,2,3]
// Result: 6

loggedSum(10, 20);
// Calling sum with args: [10,20]
// Result: 30

Generic function logger

function withLogging(fn, name = fn.name) {
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      console.log(`[${name}] called with:`, args);
      const result = Reflect.apply(target, thisArg, args);
      console.log(`[${name}] returned:`, result);
      return result;
    }
  });
}

const add = (a, b) => a + b;
const loggedAdd = withLogging(add);

loggedAdd(3, 4);
// [add] called with: [3, 4]
// [add] returned: 7

Reflect

Reflect provides the default implementations for all proxy trap operations. It mirrors the proxy handler methods exactly — same names, same signatures.

// These are equivalent:
target[prop]             ↔   Reflect.get(target, prop)
target[prop] = value     ↔   Reflect.set(target, prop, value)
delete target[prop]      ↔   Reflect.deleteProperty(target, prop)
prop in target           ↔   Reflect.has(target, prop)
new Target(...args)      ↔   Reflect.construct(Target, args)
fn.apply(obj, args)      ↔   Reflect.apply(fn, obj, args)

Why use Reflect?

The main reason — inside a proxy trap, use Reflect to forward the operation to the target with the correct behavior, especially when dealing with inherited properties and this binding.

const user = {
  _name: "Ali",
  get name() {
    return this._name; // 'this' matters here
  }
};

// ❌ Without Reflect — this breaks getter
const proxy1 = new Proxy(user, {
  get(target, prop) {
    return target[prop]; // 'this' inside getter is target, not proxy
  }
});

// ✅ With Reflect — preserves correct this binding
const proxy2 = new Proxy(user, {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver); // receiver = proxy
  }
});

receiver is the proxy itself. Reflect.get passes it as this to any getter — so getters on the target work correctly.

The canonical proxy pattern

When you want to intercept an operation and still do the default thing — use Reflect:

const handler = {
  get(target, prop, receiver) {
    console.log(`get: ${prop}`);
    return Reflect.get(target, prop, receiver); // do default get
  },

  set(target, prop, value, receiver) {
    console.log(`set: ${prop} = ${value}`);
    return Reflect.set(target, prop, value, receiver); // do default set
  },

  deleteProperty(target, prop) {
    console.log(`delete: ${prop}`);
    return Reflect.deleteProperty(target, prop); // do default delete
  }
};

Real Use — Reactive Data

One of the most powerful uses of Proxy is building reactive data — objects that automatically trigger updates when they change. This is exactly how Vue 3's reactivity system works.

function reactive(obj, onChange) {
  return new Proxy(obj, {
    get(target, prop, receiver) {
      const value = Reflect.get(target, prop, receiver);

      // If value is an object — make it reactive too
      if (typeof value === "object" && value !== null) {
        return reactive(value, onChange);
      }

      return value;
    },

    set(target, prop, value, receiver) {
      const oldValue = target[prop];
      const result = Reflect.set(target, prop, value, receiver);

      if (oldValue !== value) {
        onChange(prop, oldValue, value);
      }

      return result;
    }
  });
}

const state = reactive(
  { user: { name: "Ali", age: 22 }, theme: "light" },
  (prop, oldVal, newVal) => {
    console.log(`Changed: ${prop} — ${oldVal} → ${newVal}`);
    updateUI();
  }
);

function updateUI() {
  console.log("UI updated!");
}

state.theme = "dark";
// Changed: theme — light → dark
// UI updated!

state.user.name = "Sara";
// Changed: name — Ali → Sara
// UI updated!

The proxy intercepts every property write — no matter how deep — and triggers the update. This is a simplified version of Vue 3's reactive core.


Real Use — Read-Only Object

function readOnly(obj) {
  return new Proxy(obj, {
    set(target, prop) {
      throw new TypeError(`Cannot set property "${prop}" — object is read-only`);
    },

    deleteProperty(target, prop) {
      throw new TypeError(`Cannot delete property "${prop}" — object is read-only`);
    },

    get(target, prop, receiver) {
      const value = Reflect.get(target, prop, receiver);
      if (typeof value === "object" && value !== null) {
        return readOnly(value); // nested objects are also read-only
      }
      return value;
    }
  });
}

const config = readOnly({
  apiUrl: "https://api.example.com",
  settings: { theme: "dark", language: "en" }
});

console.log(config.apiUrl);           // https://api.example.com
config.apiUrl = "changed";            // ❌ TypeError
delete config.apiUrl;                 // ❌ TypeError
config.settings.theme = "light";      // ❌ TypeError — nested too

Real Use — API Client With Proxy

function createApiClient(baseUrl) {
  return new Proxy({}, {
    get(target, resource) {
      // proxy.users → creates a resource handler
      return new Proxy({}, {
        get(_, method) {
          // proxy.users.get → returns a function
          return async (id, options = {}) => {
            const url = id
              ? `${baseUrl}/${resource}/${id}`
              : `${baseUrl}/${resource}`;

            const response = await fetch(url, {
              method: method.toUpperCase(),
              headers: { "Content-Type": "application/json" },
              ...options
            });

            if (!response.ok) {
              throw new Error(`${method.toUpperCase()} ${url} failed: ${response.status}`);
            }

            return response.json();
          };
        }
      });
    }
  });
}

const api = createApiClient("https://jsonplaceholder.typicode.com");

// Magically works for any resource and method
const users = await api.users.get();
const user = await api.users.get(1);
const posts = await api.posts.get();
const post = await api.posts.get(1);

console.log(user.name);  // Leanne Graham
console.log(post.title); // sunt aut facere...

No manually defined methods for each resource — the Proxy intercepts any property access and creates the right API call dynamically.


Proxy Limitations

// Proxies cannot intercept:
// - Private class fields (#field) — not accessible through normal property access
// - Strict equality comparison (===) — proxy !== target
// - typeof operator
// - WeakMap/WeakSet keys — identity-based, proxy has different identity

class User {
  #name;
  constructor(name) { this.#name = name; }
  getName() { return this.#name; }
}

const user = new User("Ali");
const proxy = new Proxy(user, {});

console.log(proxy.getName()); // ✅ works — calls through proxy
// Private fields accessed through methods work fine

Summary

  • Proxy wraps an object and intercepts operations through trapsget, set, has, deleteProperty, apply, construct
  • Handler — the object containing traps — each trap corresponds to one JavaScript operation
  • get trap — intercept property reads — add defaults, logging, virtual properties
  • set trap — intercept property writes — validate data, track changes — must return true
  • has trap — intercept the in operator — custom membership checks
  • apply trap — intercept function calls — logging, timing, memoization
  • Reflect provides default implementations for all trap operations — same names, same behavior
  • Use Reflect.get(target, prop, receiver) inside traps to preserve correct this binding for getters
  • The canonical pattern — intercept the operation, do something extra, forward to Reflect
  • Real uses — validation, reactive data, read-only objects, dynamic APIs, logging
  • Proxies cannot intercept private class fields, strict equality, or typeof

On this page