JavaScript Proxy and Revocable Proxy Explained
JavaScript Proxy lets you wrap an object and intercept operations such as reading properties, writing properties, checking for keys, or calling functions. A revocable proxy adds one extra ability: you can later disable the proxy so it no longer works.
Quick answer: Use Proxy when you need to control or observe how an object is used. Use Proxy.revocable() when that control should be temporary and you want a built-in way to turn it off.
Difficulty: Intermediate
You'll understand this better if you know: basic objects, property access with . and [], functions, and how JavaScript returns values from methods.
1. What Is JavaScript Proxy and Revocable Proxy?
A Proxy is a wrapper around another value, usually an object or function. Instead of interacting with the original target directly, code interacts with the proxy, and the proxy can decide how to respond.
- It can intercept reads, writes, deletions, and other operations.
- It always works with a target and a handler.
- The handler contains traps, which are methods for specific operations.
- A revocable proxy is a proxy that can be disabled later through a revoke function.
This makes proxies useful for validation, logging, access control, virtualization, and advanced metaprogramming.
2. Why Proxy and Revocable Proxy Matter
Proxy matters because it gives you a standard way to customize object behavior without changing every place that uses the object. You can protect invariants, provide lazy behavior, and observe usage in a single place.
Revocable proxy matters when temporary access is important. For example, you may want to expose an object only during a task and then invalidate it after cleanup, reducing the chance of accidental later use.
3. Basic Syntax or Core Idea
Creating a proxy
At minimum, a proxy needs a target and a handler object. If the handler is empty, the proxy behaves like the target.
const target = { name: "Ada" };const proxy = new Proxy(target, {});This proxy forwards every operation to target because no traps are defined.
Adding a trap
A trap is a method like get or set that intercepts a particular operation.
const user = { name: "Ada" };const proxy = new Proxy(user, { get(target, property, receiver) { return Reflect.get(target, property, receiver); }});Using Reflect inside a trap is a common pattern because it performs the default operation safely and predictably.
Creating a revocable proxy
Proxy.revocable() returns an object with two properties: proxy and revoke.
const { proxy, revoke } = Proxy.revocable({ id: 1 }, {});console.log(proxy.id);revoke();After revocation, any attempt to use proxy throws a TypeError.
4. Step-by-Step Examples
Example 1: Logging property reads
This proxy prints whenever code reads a property. It is helpful for debugging and understanding object access patterns.
const settings = { theme: "dark", fontSize: 16 };const loggedSettings = new Proxy(settings, { get(target, property, receiver) { console.log("Reading", property); return Reflect.get(target, property, receiver); }});console.log(loggedSettings.theme);This shows how a proxy can observe access without changing the data itself.
Example 2: Validating writes
Proxies are often used to validate values before they are stored. Here, the proxy rejects invalid ages.
const person = { age: 20 };const validatedPerson = new Proxy(person, { set(target, property, value, receiver) { if (property === "age" && (value < 0 || value > 130)) { throw new RangeError("age must be between 0 and 130"); } return Reflect.set(target, property, value, receiver); }});validatedPerson.age = 25;Here the set trap enforces a rule before the write reaches the target.
Example 3: Hiding private fields by convention
JavaScript does not have true private object keys in plain objects, but a proxy can hide properties that begin with an underscore.
const account = { name: "Lin", _token: "secret" };const safeAccount = new Proxy(account, { get(target, property, receiver) { if (String(property).startsWith("_")) { return undefined; } return Reflect.get(target, property, ); }});console.log(safeAccount.name);console.log(safeAccount._token);This pattern is useful for UI-facing wrappers, but it is not a security boundary by itself.
Example 4: Temporarily exposing an object
Revocable proxies are useful when code should only use an object during a limited period. After cleanup, the proxy is disabled.
const { proxy: session, revoke } = Proxy.revocable({ userId: 42 }, {});console.log(session.userId);revoke();After revoke(), the proxy can no longer be used for reads, writes, or any other operation.
5. Practical Use Cases
- Validating form-like data before it is stored in a state object.
- Building debugging tools that record property access and mutation.
- Implementing transparent memoization or lazy-loading behavior.
- Creating view models that present a safer or simpler interface than the underlying data.
- Protecting capability-based APIs by revoking access after a task completes.
- Supporting advanced libraries that need to observe object behavior without changing call sites.
6. Common Mistakes
Mistake 1: Forgetting to return a value from a trap
A trap must usually return something meaningful. If a get trap returns undefined by accident, property reads appear broken.
Problem: The trap logs access but never forwards the original value, so every property lookup returns undefined.
const user = { name: "Ada" };const proxy = new Proxy(user, { get(target, property) { console.log(property); }});Fix: Return the default result with Reflect.get() or a custom value only when you really want to change behavior.
const fixedProxy = new Proxy(user, { get(target, property, receiver) { console.log(property); return Reflect.get(target, property, ); }});The corrected version works because it preserves the target’s normal behavior while still observing access.
Mistake 2: Returning false from a set trap when assignment should succeed
If a set trap returns false, JavaScript treats the assignment as failed. In strict mode, this can become a runtime error.
Problem: The trap blocks every write, so even valid updates fail and can throw a TypeError in strict code.
"use strict";const target = { count: 0 };const proxy = new Proxy(target, { set() { return false; }});proxy.count = 1;Fix: Forward valid assignments and only block the ones that violate your rule.
const safeProxy = new Proxy(target, { set(target, property, value, ) { if (property === "count" && value < 0) { return false; } return Reflect.set(target, property, value, ); }});The corrected version works because it only rejects invalid values and allows normal writes through.
Mistake 3: Using a proxy after revocation
Revocable proxies stop working once revoke() is called. Any later access throws immediately.
Problem: The code keeps a reference to the proxy and tries to use it after revocation, which causes a TypeError such as Cannot perform 'get' on a proxy that has been revoked.
const { proxy, revoke } = Proxy.revocable({ name: "Ada" }, {});revoke();console.log(proxy.name);Fix: Revoke only after you are completely finished with the proxy, and remove or replace any remaining references.
const { proxy: session, revoke: disableSession } = Proxy.revocable({ name: "Ada" }, {});console.log(session.name);disableSession();The fixed version works because the proxy is used only before it is intentionally disabled.
7. Best Practices
Practice 1: Prefer Reflect for default behavior
Reflect keeps traps small and preserves normal JavaScript semantics, which reduces accidental bugs.
const proxy = new Proxy({ count: 1 }, { get(target, property, ) { return Reflect.get(target, property, ); }});This is better than manually indexing into the target because it respects getters and inheritance correctly.
Practice 2: Keep proxy logic focused
One proxy should usually do one job, such as validation or logging. A large handler becomes hard to test and reason about.
const priceProxy = new Proxy({ price: 10 }, { set(target, property, value, ) { if (property === "price" && value < 0) { throw new RangeError("price cannot be negative"); } return Reflect.set(target, property, value, ); }});This is easier to maintain than mixing validation, logging, and authorization in one handler.
Practice 3: Be careful when proxying objects with methods
Methods can rely on the correct this value. Using the trap arguments correctly helps avoid surprising behavior.
const counter = { count: 0, increment() { return this.count += 1; }};const proxy = new Proxy(counter, { get(target, property, ) { return Reflect.get(target, property, ); }});Passing the receiver into Reflect.get() helps methods and getters see the proxy as expected.
8. Limitations and Edge Cases
- Proxies only intercept operations that go through the proxy. Direct access to the original target is not intercepted.
- They cannot change private class fields because private field access is not mediated through proxy traps.
- Some built-in objects and exotic behaviors can be tricky to proxy correctly, especially when invariants must be preserved.
- Proxies can add overhead, so they are not ideal for hot code paths that run millions of times.
- Revoked proxies are permanently disabled; there is no undo operation.
- Proxy traps must obey JavaScript invariants, or the engine may throw errors if the proxy lies about non-configurable or non-writable properties.
Warning: Do not treat a proxy as a security boundary for secrets stored in the same process. If code can still reach the original target, it can bypass the proxy completely.
9. Practical Mini Project
In this mini project, we will build a small settings guard that validates updates and can be disabled after the settings screen closes.
The proxy will:
- allow only known settings keys,
- validate values before storing them,
- and be revocable when the settings session ends.
function createSettingsSession(initialSettings) { const allowedKeys = ["theme", "fontSize", "notifications"]; const guarded = Proxy.revocable(initialSettings, { set(target, property, value, ) { if (!allowedKeys.includes(String(property))) { throw new ReferenceError("Unknown setting: " + String(property)); } if (property === "fontSize" && (value < 10 || value > 32)) { throw new RangeError("fontSize must be between 10 and 32"); } return Reflect.set(target, property, value, ); } }); return { settings: guarded.proxy, close() { guarded.revoke(); } };}const { settings, close } = createSettingsSession({ theme: "dark", fontSize: 16, notifications: true});settings.fontSize = 18;close();This example combines validation with temporary access control, which is a realistic reason to use a revocable proxy.
10. Key Points
- Proxy wraps a target and intercepts operations through traps.
- Reflect is the safest way to forward default behavior inside traps.
- Proxy.revocable() returns a proxy plus a function that permanently disables it.
- Proxies are useful for validation, logging, and behavior customization, but they add complexity.
- Revoked proxies throw a TypeError if used after revocation.
- Proxy logic should stay small and focused so that behavior is predictable.
11. Practice Exercise
Create a proxy that wraps a profile object with these rules:
- Reading fullName should return the first and last name joined with a space.
- Writing to age should allow only whole numbers from 0 to 120.
- Any attempt to access password should return undefined.
- Use Proxy.revocable() so the proxy can be disabled after use.
Expected output: A working proxy that formats fullName, validates age, hides password, and throws after revocation.
Hint: Use get for reads, set for writes, and Reflect to forward allowed operations.
function createProfileProxy(profile) { const guarded = Proxy.revocable(profile, { get(target, property, ) { if (property === "password") { return undefined; } if (property === "fullName") { return target.firstName + " " + target.lastName; } return Reflect.get(target, property, ); }, set(target, property, value, ) { if (property === "age") { if (!Number.isInteger(value) || value < 0 || value > 120) { throw new RangeError("age must be an integer between 0 and 120"); } } return Reflect.set(target, property, value, ); } }); return guarded;}const profile = createProfileProxy({ firstName: "Ada", lastName: "Lovelace", age: 36, password: "secret"});console.log(profile.fullName);profile.age = 40;console.log(profile.password);This solution works because each trap handles exactly one rule and forwards safe operations with Reflect.
12. Final Summary
JavaScript Proxy gives you a powerful way to intercept operations on objects and functions. You can use it to log access, validate input, customize lookup behavior, and create advanced abstractions that are hard to build with plain objects alone.
Proxy.revocable() adds a lifecycle control layer. It is especially useful when an object should only be available temporarily, because revocation permanently disables the proxy and helps prevent accidental use after a task ends.
If you are learning proxies for the first time, start with get and set, then practice forwarding behavior with Reflect. After that, explore other traps and build small wrappers around real objects so the rules feel practical rather than abstract.