Proxy & Reflect in JavaScript

Why should you care about Proxy and Reflect in JavaScript?

Proxy lets you intercept operations on an object or function. Reflect provides methods for performing many of those operations using JavaScript's standard behavior. Together, they are useful for validation, safe defaults, logging, and controlled access.

Advanced topic: Learn objects, functions, and error handling first. Proxy and Reflect change how normal object operations work, so use them when you need an explicit boundary for validation, logging, or access control. Start with the practical validation example below; the later traps and receiver details can wait.

A Proxy sits between your code and an object or function. It can intercept operations such as reading, writing, deleting, or calling the target.

JavaScript introduced Proxy in ES6 (2015). Before that, there was no built-in way to intercept property access on an object.

Here is what happens without a Proxy - direct access with no control:

javascript

// Without Proxy -- direct access, no control
const config = { theme: "dark", fontSize: 16 };
console.log(config.theme); // "dark"
config.fontSize = -5; // No error! Bad data accepted silently.
console.log(config.fontSize); // -5

Proxy lets you add a layer of control around an object. Depending on the handler, it can pass operations through, block them, or change how they behave.

The syntax for creating a Proxy is:

javascript

const proxy = new Proxy(target, handler)
  • target - the original object or function
  • handler - an object containing trap methods

If the handler is empty {}, the proxy just passes everything through - no changes at all.

javascript

const config = { theme: "dark", fontSize: 16 };

// An empty handler -- everything passes through unchanged
const proxy = new Proxy(config, {});

console.log(proxy.theme); // "dark" -- same as config.theme
proxy.fontSize = 20;
console.log(config.fontSize);  // 20 -- the original object is updated

Notice that reading or writing through the proxy actually affects the original target object. They are linked - the proxy does not make a copy.

Traps are methods inside the handler object. Each trap intercepts a specific operation on the target.

The most common traps are:

  • get - intercepts property reading
  • set - intercepts property writing
  • has - intercepts the in operator
  • deleteProperty - intercepts delete
  • apply - intercepts function calls (when target is a function)
  • ownKeys - intercepts requests for the target's property keys
  • getOwnPropertyDescriptor - intercepts requests for a property descriptor
  • construct - intercepts constructor calls when the target is a function

Functions can be proxied too. For example, apply can log a function call, and construct can intercept new SomeFunction().

The get trap runs when a property is read through the Proxy:

javascript

const config = { theme: "dark", fontSize: 16 };

const proxy = new Proxy(config, {
  get(target, property) {
    console.log(`Someone read: ${property}`);
    return target[property];  // return the real value
  }
});

console.log(proxy.theme);
// Logs: "Someone read: theme"
// Then: "dark"

console.log(proxy.language);
// Logs: "Someone read: language"
// Then: undefined (property doesn't exist)

Here is a practical validation example for a settings object. The set trap checks the new value before saving it. Valid data is stored with Reflect.set; invalid data throws an error that the caller can catch.

javascript

const config = { theme: "dark", fontSize: 16 };

const proxy = new Proxy(config, {
  set(target, property, value) {
    if (property === "fontSize" && typeof value !== "number") {
      throw new TypeError("fontSize must be a number!");
    }
    if (property === "fontSize" && (value < 8 || value > 72)) {
      throw new RangeError("fontSize must be between 8 and 72!");
    }
    return Reflect.set(target, property, value); // apply the normal write
  }
});

try {
  proxy.fontSize = 20;   // valid: the value is stored
  console.log(config.fontSize); // 20

  proxy.fontSize = "large"; // invalid: TypeError
} catch (error) {
  console.log(error.message); // fontSize must be a number!
}

try {
  proxy.fontSize = 2; // invalid: RangeError
} catch (error) {
  console.log(error.message); // fontSize must be between 8 and 72!
}
  1. Assignment starts the trap: proxy.fontSize = value calls set with the target, property name, and new value.
  2. Validation runs first: the trap rejects non-numbers and numbers outside the allowed range.
  3. The normal write still happens: Reflect.set stores valid data and returns whether the operation succeeded.

If a trap is not defined in the handler, the operation works normally on the target. Proxy traps cannot report impossible results that violate JavaScript's rules for the target object.

Validation - intercept set to check data before it is stored (shown in the previous section).

Default values - intercept get to return a fallback when a property is undefined:

javascript

const settings = { theme: "dark" };

const proxy = new Proxy(settings, {
  get(target, property) {
    // If property doesn't exist, return a default instead of undefined
    return property in target ? target[property] : "default";
  }
});

console.log(proxy.theme);    // "dark"
console.log(proxy.language); // "default" (not undefined)
console.log(proxy.fontSize); // "default"

Logging / observing - intercept operations to log what is happening, which can help with debugging.

Read-only objects - intercept set to prevent top-level changes:

javascript

const config = { apiUrl: "https://api.example.com", version: "1.0" };

const readOnly = new Proxy(config, {
  set() {
    throw new Error("This object is read-only. You cannot change it.");
  }
});

console.log(readOnly.apiUrl); // "https://api.example.com"
// readOnly.apiUrl = "something"; // throws Error

This example is shallow. If config contains nested objects, those nested objects can still be changed unless you wrap them in their own proxies too.

Reflect is a built-in JavaScript object with methods for performing common object operations. It is not a class or constructor.

Inside a Proxy trap, Reflect is commonly used to perform the normal operation while preserving JavaScript's standard behavior. For example, Reflect.set(target, property, value) performs the normal property write.

Why use Reflect instead of doing it manually?

  • Its methods mirror corresponding JavaScript object operations
  • Methods such as Reflect.set return a useful success value
  • It provides a clear way to pass the normal operation through a Proxy trap

Key Reflect methods include:

  • Reflect.get(target, property) - performs a property read
  • Reflect.set(target, property, value) - performs a property write
  • Reflect.has(target, property) - checks whether a property exists
  • Reflect.deleteProperty(target, property) - deletes a property
  • Reflect.ownKeys(target) and Reflect.getOwnPropertyDescriptor(target, property) work with keys and property details
  • Reflect.apply(fn, thisArg, args) and Reflect.construct(fn, args) work with function calls and constructor calls

Here is a simple example using Reflect directly (without a Proxy):

javascript

const config = { theme: "dark", fontSize: 16 };

// Using Reflect directly (without Proxy)
console.log(Reflect.get(config, "theme"));   // "dark"

Reflect.set(config, "fontSize", 20);
console.log(config.fontSize); // 20

console.log(Reflect.has(config, "theme"));  // true
console.log(Reflect.has(config, "language")); // false

Reflect.deleteProperty(config, "fontSize");
console.log(config.fontSize); // undefined

On its own, Reflect is not very exciting. Its real power shows up when combined with Proxy.

Proxy intercepts the operation. Reflect performs the normal operation. This combination lets you add logic while keeping JavaScript's usual behavior for the underlying object.

Here is a logging proxy that still works correctly:

javascript

const config = { theme: "dark", fontSize: 16 };

const proxy = new Proxy(config, {
  get(target, property, receiver) {
    console.log(`Reading: ${property}`);
    return Reflect.get(target, property, receiver); // safe default
  },
  set(target, property, value, receiver) {
    console.log(`Writing: ${property} = ${value}`);
    return Reflect.set(target, property, value, receiver); // safe default
  }
});

proxy.theme;          // Logs: "Reading: theme"
proxy.fontSize = 20;  // Logs: "Writing: fontSize = 20"
console.log(config.fontSize); // 20

Advanced: The receiver Parameter

The receiver parameter is important when a Proxy works with getters and setters. It determines the object used as this when the getter or setter is called through the Proxy.

In short: Proxy intercepts the operation, and Reflect performs the normal operation.

Advanced: Proxy.revocable() creates a Proxy that can later be disabled.

  • Proxy is an advanced feature: it wraps an object and lets you intercept operations like reading, writing, deleting, and more.
  • You create a proxy with new Proxy(target, handler). The target is the original object; the handler has trap functions.
  • Traps are methods in the handler: get, set, has, deleteProperty, apply, and more.
  • If a trap is not defined, the operation passes through to the original object unchanged.
  • Common uses: input validation, default values, read-only objects, logging, and observing changes.
  • Reflect is a built-in object with methods for many fundamental operations such as Reflect.get, Reflect.set, Reflect.has, Reflect.ownKeys, and more.
  • Inside Proxy traps, Reflect is commonly used to perform the matching standard operation.
  • Proxies can wrap objects or functions, but they do not copy the target. Use them when the added control is worth the extra complexity.
What's next? Continue with the Web Workers tutorial, or explore Generators & Iterators in the sidebar.

Reviewed by

SimplyJavaScript Editorial Team

Technical editors and JavaScript educators with hands-on experience building frontend projects, writing learning material, and reviewing tutorials for clarity, accuracy, and beginner-friendly guidance.