Academic Block

Proxies & Reflect Get Certified

Proxies and Reflect are JavaScript’s metaprogramming tools. A Proxy lets you intercept and customize fundamental operations on an object (reading, writing, deleting, etc.). Reflect provides the matching default behaviors so you can forward operations cleanly from inside a trap.

What Is a Proxy?

A Proxy wraps a target object with a handler. The handler contains “traps” — functions that run whenever a specific operation is performed on the proxy.

What this code does: Creates a basic Proxy that logs every property access through the get trap.

const person = { name: "Michael", age: 32 };

const loggedPerson = new Proxy(person, {
    get(target, property) {
        console.log("Accessed property:", property);
        return target[property];
    }
});

console.log(loggedPerson.name);
console.log(loggedPerson.age);

The get Trap: Intercepting Property Reads

The get trap runs whenever a property is read. You can return a custom value, provide defaults, or hide internal data.

What this code does: Returns a default message when a missing property is accessed, instead of undefined.

const settings = { theme: "dark" };

const safeSettings = new Proxy(settings, {
    get(target, property) {
        if (property in target) {
            return target[property];
        }
        return `(no value for "${property}")`;
    }
});

console.log(safeSettings.theme);
console.log(safeSettings.language);

The set Trap: Intercepting Property Writes

The set trap runs on every assignment. Returning true signals that the assignment succeeded; returning false (in strict mode) causes a TypeError.

What this code does: Intercepts property writes with a set trap to validate that health cannot drop below zero.

const player = { health: 100 };

const guardedPlayer = new Proxy(player, {
    set(target, property, value) {
        if (property === "health" && (typeof value !== "number" || value < 0)) {
            throw new TypeError("Health must be a non-negative number.");
        }
        target[property] = value;
        return true;
    }
});

guardedPlayer.health = 75;
console.log(guardedPlayer.health);

try {
    guardedPlayer.health = -10;
} catch (err) {
    console.log("Error:", err.message);
}

The has Trap: Customizing the in Operator

The has trap runs whenever the in operator is used to check a property, letting you hide certain properties from that check.

What this code does: Hides internal properties (those starting with _) from the in operator.

const userRecord = { username: "michael_dev", _internalId: "u-8842" };

const publicUserRecord = new Proxy(userRecord, {
    has(target, property) {
        if (property.startsWith("_")) {
            return false;
        }
        return property in target;
    }
});

console.log("username" in publicUserRecord);
console.log("_internalId" in publicUserRecord);

The deleteProperty Trap

The deleteProperty trap runs whenever the delete operator is used, letting you block deletion of specific properties.

What this code does: Protects the apiUrl property so it cannot be deleted.

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

const protectedConfig = new Proxy(config, {
    deleteProperty(target, property) {
        if (property === "apiUrl") {
            console.log("Cannot delete apiUrl — it's required.");
            return false;
        }
        delete target[property];
        return true;
    }
});

delete protectedConfig.apiUrl;
delete protectedConfig.version;
console.log(protectedConfig);

What Is Reflect?

The Reflect object provides methods that mirror the default behavior a Proxy trap would otherwise have to reimplement manually — Reflect.get(), Reflect.set(), Reflect.has(), and more.

What this code does: Demonstrates Reflect.get, Reflect.set, Reflect.has, and Reflect.ownKeys.

const book = { title: "The Long Way Home", pages: 288 };

console.log(Reflect.get(book, "title"));

Reflect.set(book, "pages", 300);
console.log(book.pages);

console.log(Reflect.has(book, "title"));
console.log(Reflect.ownKeys(book));

On its own, Reflect doesn’t do anything you couldn’t already do with regular syntax — its real value shows up when paired with Proxy traps.

Using Reflect Inside Proxy Traps

Inside a trap, using the matching Reflect method to perform the default behavior is safer and more consistent than manually reimplementing it, especially for edge cases like inherited properties.

What this code does: Combines a Proxy with Reflect so the trap logs the operation and then forwards it to the default behavior.

const inventory = { apples: 12, bananas: 6 };

const loggedInventory = new Proxy(inventory, {
    get(target, property, receiver) {
        console.log("Getting:", property);
        return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
        console.log("Setting:", property, "to", value);
        return Reflect.set(target, property, value, receiver);
    }
});

loggedInventory.apples;
loggedInventory.bananas = 10;

Revocable Proxies

Proxy.revocable() creates a proxy that can be permanently disabled later, which is useful for revoking access to an object once it’s no longer needed.

What this code does: Creates a revocable proxy and shows that after revoke() any access throws a TypeError.

const sessionData = { token: "abc123" };

const { proxy, revoke } = Proxy.revocable(sessionData, {});

console.log(proxy.token);

revoke();

try {
    console.log(proxy.token);
} catch (err) {
    console.log("Error:", err.message);
}

After calling revoke(), every operation on proxy throws a TypeError, even simply reading a property.

Common Proxy Traps

Trap Triggered By Matching Reflect Method
get Reading a property Reflect.get()
set Assigning a property Reflect.set()
has The in operator Reflect.has()
deleteProperty The delete operator Reflect.deleteProperty()
ownKeys Object.keys() and similar Reflect.ownKeys()

Practical Example

Let’s combine a few traps to build a simple validated form object where each field is checked against a schema.

What this code does: Builds a validated form object where each field is checked against a schema inside the set trap.

function createValidatedForm(schema) {
    const data = {};
    return new Proxy(data, {
        set(target, property, value) {
            const validator = schema[property];
            if (validator && !validator(value)) {
                console.log("Rejected invalid value for", property + ":", value);
                return true;
            }
            target[property] = value;
            return true;
        }
    });
}

const signupForm = createValidatedForm({
    age: value => typeof value === "number" && value >= 18,
    email: value => typeof value === "string" && value.includes("@")
});

signupForm.age = 25;
signupForm.age = 12;
signupForm.email = "not-an-email";
console.log(signupForm);

Common Mistakes With Proxies and Reflect

  • Forgetting to return a value from the set trap, which can cause silent or unexpected failures in strict mode.
  • Manually reimplementing default behavior inside a trap instead of delegating to the matching Reflect method.
  • Modifying the original target object directly outside the proxy, expecting the proxy’s traps to somehow still apply.
  • Using a proxy after it has been revoked, and being surprised by the resulting TypeError.
  • Overusing proxies for simple cases where a plain function or getter/setter would be clearer and perform better.

Best Practices for Proxies and Reflect

  • Use the matching Reflect method inside a trap whenever you want the default behavior.
  • Keep trap logic focused on one concern — validation, logging, or defaults.
  • Reach for a Proxy when you genuinely need to intercept operations dynamically; for known, fixed properties, getters and setters are often simpler.
  • Use Proxy.revocable() when you need to be able to permanently cut off access to an object later.
  • Document what a Proxy customizes, since its behavior isn’t visible just by looking at the target object.

Quick Summary

  • A Proxy wraps a target object with a handler containing traps that intercept operations like reading, writing, and deleting properties.
  • Common traps include get, set, has, and deleteProperty.
  • Reflect provides methods matching the default behavior of these operations, meant to be used inside traps.
  • Proxy.revocable() creates a proxy that can later be permanently disabled.
  • Proxies are useful for validation, logging, default values, and controlling access to an object’s properties.

Try It Yourself

Modify the code below. Try changing the validation rule, or adding a new field with its own validator.

What this code does: A practice example that guards a product price so negative values are rejected while valid prices are stored normally.

const product = { price: 0 };

const guardedProduct = new Proxy(product, {
    set(target, property, value) {
        if (property === "price" && value < 0) {
            console.log("Price cannot be negative.");
            return true;
        }
        target[property] = value;
        return true;
    }
});

guardedProduct.price = 49.99;
console.log(guardedProduct.price);

guardedProduct.price = -5;
console.log(guardedProduct.price);
Tip: A helpful mental model: a Proxy is a customizable gatekeeper in front of an object, and Reflect is the gatekeeper’s default rulebook — you only need to override the specific rules you actually want to change.

Understanding Proxies and Reflect is useful before moving on to modules, design patterns, and more advanced metaprogramming concepts in JavaScript.

🧪 Test Your JavaScript Code

Edit the code on the left and click "Run Code" to see the console output on the right.

📝 JavaScript Code
👁 Console Output

Click "Run Code" to see the output here.

Ctrl + Enter to run code Esc to close