Back to News & Insights
JavaScript August 24, 2026 · 13 min read

JavaScript Proxy and Reflect: The Complete Guide

You set user.age = -5 on a plain object and nothing stops you. No error, no warning — the object...

JavaScript Proxy and Reflect: The Complete Guide

You set user.age = -5 on a plain object and nothing stops you. No error, no warning — the object silently accepts a value that makes no sense, and the bug surfaces three files away, in whatever code trusted age to be a real number. Every framework that seems to "just know" when your state changed — Vue's reactivity, a validation library that rejects bad input at the boundary, an ORM that lazy-loads a relation the moment you touch it — is solving this exact problem with one native JavaScript feature that most tutorials skip past in a paragraph: Proxy.

By the end of this guide you'll be able to: Explain what a Proxy actually is — a stand-in that intercepts operations on an object, not a copy or a wrapper class Write get, set, has, deleteProperty, and ownKeys traps to validate, hide, and log property access Use Reflect correctly, and explain the one bug it exists to prevent Build a small reactive-state system — the same mechanism Vue 3 uses under the hood Recognize the invariants, gotchas, and performance tradeoffs that catch people in production

Who this is for: you write JavaScript or TypeScript day to day, you've used objects and classes comfortably, and you've heard of Proxy but never reached for it — or you've seen Reflect.get(target, prop, receiver) in someone else's code and wondered why they didn't just write target[prop].

Contents Why JavaScript Proxy exists The mental model: a checkpoint in front of every operation Stage 1: your first proxy — a get and set trap Stage 2: validation without a setter for every field Stage 3: Reflect, and why the receiver matters Stage 4: hiding and protecting properties Stage 5: building a tiny reactive system Edge cases and gotchas Best practices FAQ Cheat sheet Key takeaways

Here's the naive fix for "validate this field whenever it's set" — a hand-written getter/setter pair:

This works — for age. Add email, score, and role, and you're maintaining four nearly identical getter/setter pairs, each one a place to forget the check. Miss one, and that field silently accepts garbage, exactly like the plain object at the top of this article. The validation logic is also scattered per-field instead of living in one place you can audit.

What you actually want is a way to say "run this code whenever any property is read or written on this object" — one interception point, not N hand-written pairs. That's precisely what Proxy gives you, and Reflect is the toolkit that makes writing traps correctly possible.

The mental model: a Proxy is not the object — it's a stand-in that sits in front of the real object (the target) and intercepts a fixed set of fundamental operations: reading a property, writing one, checking in, deleting, listing keys, and a few others. Each operation you intercept is called a trap. If you don't define a trap for an operation, it passes straight through to the target, unchanged — and Reflect is how you perform that same "pass it through" behavior explicitly, from inside a trap you did define.

Think of it like a customs checkpoint at a border. Most traffic (an operation with no trap) just walks through untouched. But for the operations you care about, you install an inspector (the trap function) who can log the traffic, reject it, alter it, or wave it through — and when they wave it through, they're not improvising; they're calling the same official procedure (Reflect) that would have run automatically if no inspector were there at all.

Key concept: one get/set pair intercepts every property on the object, in one place — not one pair per field. The set trap must return true (or any truthy value); return a falsy value and JavaScript throws a TypeError, because the engine treats a falsy return as "this write failed."

Now replace the User class's boilerplate with one reusable set trap and a table of rules:

Adding a validated field for email or score is now a one-line rule in the rules object, not a new getter/setter pair. The check lives in exactly one place — the set trap — no matter how many fields you validate. In TypeScript, validated is worth making generic in its own right, so the object you get back keeps the exact shape of the object you passed in — the same type-parameter-as-argument idea covered in the guide to TypeScript generics.

Stage 1's traps forwarded reads and writes with obj[prop] directly. That works for plain data, but it quietly breaks once a getter and a prototype chain are involved — and this is the exact bug Reflect exists to prevent.

obj.self should return obj — that's what a getter returning this means when you access it through obj. But the trap wrote target[prop], so the getter ran with this bound to target, not obj. The fix is to forward the operation with Reflect.get, which takes a third argument — the receiver — and passes it through as this:

Key concept: every trap's default behavior — what would happen with no trap at all — is exactly what its matching Reflect method does. target[prop] looks equivalent, but it silently drops the receiver; Reflect.get(target, prop, receiver) is the one that actually replicates the engine's own default.

Reflect isn't a Proxy-only feature — it mirrors all 13 of the fundamental object operations (get, set, has, deleteProperty, ownKeys, getPrototypeOf, setPrototypeOf, isExtensible, preventExtensions, defineProperty, getOwnPropertyDescriptor, apply, construct) as plain functions instead of operators or statements. Outside a Proxy trap, that mostly matters for two things: Reflect.ownKeys(obj) gets you every own key (strings and symbols) in one call, and Reflect.construct(Ctor, args) calls a constructor with a dynamic argument list without new Ctor(...args)'s syntax constraints.

Traps aren't limited to get/set. has intercepts the in operator, deleteProperty intercepts delete, and ownKeys intercepts Object.keys, for...in, and JSON.stringify:

That last line matters: hiding a key from enumeration (ownKeys/has) is a different guarantee from blocking direct access (get). This example only hides password from listing and serialization — anyone who already knows the key name can still read it. If you want both, add a get trap that throws or returns undefined for that key.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation