Proxy and Reflect API for JavaScript Metaprogramming
Learn how to use JavaScript Proxy and Reflect to intercept operations, build validators, create observable objects, and write metaprogramming patterns.
What you'll learn
- ✓How Proxy traps intercept fundamental object operations
- ✓Using Reflect to forward default behavior cleanly
- ✓Building validation layers, observable state, and access control
Prerequisites
- •Solid JavaScript object fundamentals
- •Familiarity with getters and setters
JavaScript’s Proxy and Reflect APIs let you intercept and redefine fundamental operations on objects. Together they form the foundation of metaprogramming in JavaScript — building code that manipulates other code at runtime.
What is a Proxy?
A Proxy wraps a target object and lets you define traps — handler functions that intercept operations like property reads, writes, deletions, and function calls.
const target = { greeting: "hello" };
const handler = {
get(target, property, receiver) {
console.log(`Accessing property: ${property}`);
return Reflect.get(target, property, receiver);
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.greeting);
// "Accessing property: greeting"
// "hello"
The first argument to new Proxy() is the target object. The second is the handler containing your traps. When you interact with the proxy, the traps fire instead of the default behavior.
Why Reflect?
Reflect provides static methods that mirror every Proxy trap. Instead of manually doing target[prop], you call Reflect.get(target, prop, receiver). This is important because Reflect methods handle edge cases around prototype chains and receivers correctly.
const handler = {
get(target, prop, receiver) {
// Bad: target[prop] -- ignores receiver, breaks inheritance
// Good: Reflect.get preserves correct this binding
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`Setting ${prop} to ${value}`);
return Reflect.set(target, prop, value, receiver);
},
has(target, prop) {
console.log(`Checking if "${prop}" exists`);
return Reflect.has(target, prop);
},
deleteProperty(target, prop) {
console.log(`Deleting ${prop}`);
return Reflect.deleteProperty(target, prop);
},
};
Every Reflect method returns a boolean or the value, making it easy to compose behavior on top of defaults.
Pattern 1: Validation layer
One of the most practical uses is building a validation layer that enforces types at runtime.
function createValidatedObject(schema) {
return new Proxy(
{},
{
set(target, prop, value, receiver) {
const validator = schema[prop];
if (!validator) {
throw new Error(`Unknown property: ${prop}`);
}
if (!validator(value)) {
throw new TypeError(
`Invalid value for ${prop}: ${JSON.stringify(value)}`
);
}
return Reflect.set(target, prop, value, receiver);
},
}
);
}
const user = createValidatedObject({
name: (v) => typeof v === "string" && v.length > 0,
age: (v) => Number.isInteger(v) && v >= 0 && v <= 150,
email: (v) => typeof v === "string" && v.includes("@"),
});
user.name = "Alice"; // works
user.age = 30; // works
user.age = -5; // TypeError: Invalid value for age: -5
user.status = "active"; // Error: Unknown property: status
Pattern 2: Observable / reactive state
Frameworks like Vue use Proxy internally to make state reactive. Here is a simplified version.
function reactive(obj, onChange) {
return new Proxy(obj, {
set(target, prop, value, receiver) {
const oldValue = target[prop];
const result = Reflect.set(target, prop, value, receiver);
if (oldValue !== value) {
onChange(prop, value, oldValue);
}
return result;
},
deleteProperty(target, prop) {
const oldValue = target[prop];
const result = Reflect.deleteProperty(target, prop);
onChange(prop, undefined, oldValue);
return result;
},
});
}
const state = reactive({ count: 0 }, (prop, newVal, oldVal) => {
console.log(`${prop} changed: ${oldVal} -> ${newVal}`);
});
state.count = 1; // "count changed: 0 -> 1"
state.count = 2; // "count changed: 1 -> 2"
Pattern 3: Access control and read-only objects
You can create objects that refuse modification entirely.
function readOnly(target) {
return new Proxy(target, {
set() {
throw new Error("This object is read-only");
},
deleteProperty() {
throw new Error("This object is read-only");
},
defineProperty() {
throw new Error("This object is read-only");
},
});
}
const config = readOnly({ apiUrl: "https://api.example.com", timeout: 5000 });
console.log(config.apiUrl); // works
config.apiUrl = "hacked"; // Error: This object is read-only
Pattern 4: Default values and missing properties
Instead of returning undefined for missing properties, you can provide defaults.
function withDefaults(target, defaults) {
return new Proxy(target, {
get(target, prop, receiver) {
if (prop in target) {
return Reflect.get(target, prop, receiver);
}
return defaults[prop];
},
});
}
const settings = withDefaults(
{ theme: "dark" },
{ theme: "light", fontSize: 16, language: "en" }
);
console.log(settings.theme); // "dark" (from target)
console.log(settings.fontSize); // 16 (from defaults)
Pattern 5: Logging and profiling with the apply trap
The apply trap intercepts function calls when the proxy target is a function.
function profileFunction(fn, label) {
return new Proxy(fn, {
apply(target, thisArg, args) {
const start = performance.now();
const result = Reflect.apply(target, thisArg, args);
const duration = performance.now() - start;
console.log(`${label} took ${duration.toFixed(2)}ms`);
return result;
},
});
}
const slowSort = profileFunction((arr) => [...arr].sort(), "sort");
slowSort([3, 1, 4, 1, 5, 9, 2, 6]); // "sort took 0.04ms"
The construct trap
You can also intercept new calls with the construct trap.
function singleton(TargetClass) {
let instance = null;
return new Proxy(TargetClass, {
construct(target, args, newTarget) {
if (!instance) {
instance = Reflect.construct(target, args, newTarget);
}
return instance;
},
});
}
const SingleDB = singleton(
class Database {
constructor() {
this.id = Math.random();
}
}
);
const a = new SingleDB();
const b = new SingleDB();
console.log(a === b); // true
Revocable proxies
Proxy.revocable() creates a proxy that can be permanently disabled.
const { proxy, revoke } = Proxy.revocable(
{ secret: "data" },
{
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
}
);
console.log(proxy.secret); // "data"
revoke();
// proxy.secret -> TypeError: Cannot perform 'get' on a revoked proxy
This is useful for granting temporary access to resources.
Performance considerations
Proxies add a layer of indirection. Every trapped operation goes through your handler function. For hot paths (tight loops over thousands of iterations), this overhead can matter. Measure before wrapping performance-critical objects.
Also, proxies are not transparent — proxy !== target. Code that relies on strict identity checks may break. Always test that downstream code works correctly with proxied objects.
Summary
- Proxy intercepts 13 fundamental operations on objects via traps.
- Reflect provides matching static methods that implement the default behavior of each trap.
- Common patterns: validation, reactive state, access control, default values, logging, and singletons.
- Use
Proxy.revocable()for temporary access. - Be mindful of performance overhead on hot paths.
Proxy and Reflect unlock metaprogramming capabilities that were previously impossible in JavaScript. They are the foundation behind reactivity systems, ORMs, and validation libraries used across the ecosystem.
Related articles
- JavaScript JavaScript Proxy and Reflect: Metaprogramming Guide
Master JavaScript Proxy and Reflect API for metaprogramming with practical examples including validation, observable objects, and access control patterns.
- JavaScript JavaScript Symbols and Well-Known Symbols
Master JavaScript Symbols for unique property keys, explore well-known symbols like Symbol.iterator and Symbol.toPrimitive, and learn metaprogramming patterns.
- JavaScript JavaScript Event Loop Explained: How Async Code Really Works
Understand how the JavaScript event loop handles async operations including the call stack, microtasks, macrotasks, and execution order with practical examples.
- JavaScript JavaScript structuredClone: The Modern Deep Copy Solution
Learn how to use JavaScript structuredClone for deep copying objects, when it beats JSON.parse(JSON.stringify()), and what types it supports and cannot handle.