[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-weekly-javascript-proxy-reflect":44,"search-suggestions":60,"quiz-article-weekly-javascript-proxy-reflect":106},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"019fe776-85f8-72d0-b297-908b32200c46","weekly-javascript-proxy-reflect","PRACTICE_QUIZ","JavaScript Proxy and Reflect","Test what stuck about Proxy traps, Reflect, invariants, and the gotchas that catch people building reactive state and validation layers.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,96,100,103],{"slug":62,"name":63,"articles":64},"webdev","Webdev",64,{"slug":66,"name":67,"articles":68},"frontend","Frontend",57,{"slug":70,"name":71,"articles":72},"javascript","Javascript",54,{"slug":74,"name":75,"articles":76},"css","Css",24,{"slug":78,"name":79,"articles":80},"typescript","Typescript",10,{"slug":82,"name":83,"articles":84},"performance","Performance",9,{"slug":86,"name":87,"articles":88},"grammar","Grammar",6,{"slug":90,"name":91,"articles":92},"react","React",5,{"slug":94,"name":95,"articles":92},"tutorial","Tutorial",{"slug":97,"name":98,"articles":99},"node","Node",4,{"slug":101,"name":102,"articles":99},"html","Html",{"slug":104,"name":105,"articles":43},"ai","Ai",{"id":107,"slug":46,"title":108,"subtitle":52,"excerpt":109,"coverUrl":110,"locale":13,"readingMinutes":111,"publishedAt":112,"viewCount":113,"likeCount":19,"commentCount":19,"author":114,"vertical":119,"topic":120,"tags":122,"_count":129,"playground":131,"body":133,"bodyMd":471,"seo":472,"translationGroupId":474,"thread":475,"assessments":477,"translations":480,"quiz":482},"019fe660-932f-704d-80f6-f8646b604799","JavaScript Proxy and Reflect: The Complete Guide","Learn JavaScript Proxy and Reflect from the ground up — traps, invariants, and reactive state — with worked examples and a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Fweekly-javascript-proxy-reflect.png",15,"2026-08-24T07:06:37.948Z",81,{"id":115,"name":116,"username":117,"avatarUrl":52,"headline":118},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":70,"name":121},"JavaScript",[123,124,125,126],{"slug":70,"name":71,"color":52},{"slug":62,"name":63,"color":52},{"slug":94,"name":95,"color":52},{"slug":127,"name":128,"color":52},"programming","Programming",{"assessments":130},1,{"slug":46,"title":132},"JavaScript Proxy traps, live",{"blocks":134,"version":130},[135,139,144,147,156,159,162,177,180,183,189,192,195,198,201,204,208,211,214,217,221,225,228,231,235,238,241,244,247,250,253,256,259,263,266,270,273,276,279,282,286,289,292,295,299,302,305,314,317,320,323,326,329,332,335,338,341,344,347,350,353,356,359,362,365,368,371,424,428,431,434,437,440,443,446,454,457,460,463],{"id":136,"html":137,"type":138},"b1","\u003Cp>You set \u003Ccode>user.age = -5\u003C\u002Fcode> 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 \u003Ccode>age\u003C\u002Fcode> to be a real number. Every framework that seems to &quot;just know&quot; when your state changed — Vue&#39;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: \u003Ccode>Proxy\u003C\u002Fcode>.\u003C\u002Fp>","paragraph",{"id":140,"html":141,"text":142,"type":143,"level":31},"b2","What you&#39;ll learn","What you'll learn","heading",{"id":145,"html":146,"type":138},"b3","\u003Cp>By the end of this guide you&#39;ll be able to:\u003C\u002Fp>",{"id":148,"type":149,"items":150,"ordered":18},"b4","list",[151,152,153,154,155],"Explain what a \u003Ccode>Proxy\u003C\u002Fcode> actually is — a stand-in that intercepts operations on an object, not a copy or a wrapper class","Write \u003Ccode>get\u003C\u002Fcode>, \u003Ccode>set\u003C\u002Fcode>, \u003Ccode>has\u003C\u002Fcode>, \u003Ccode>deleteProperty\u003C\u002Fcode>, and \u003Ccode>ownKeys\u003C\u002Fcode> traps to validate, hide, and log property access","Use \u003Ccode>Reflect\u003C\u002Fcode> 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",{"id":157,"html":158,"type":138},"b5","\u003Cp>\u003Cstrong>Who this is for:\u003C\u002Fstrong> you write JavaScript or TypeScript day to day, you&#39;ve used objects and classes comfortably, and you&#39;ve heard of \u003Ccode>Proxy\u003C\u002Fcode> but never reached for it — or you&#39;ve seen \u003Ccode>Reflect.get(target, prop, receiver)\u003C\u002Fcode> in someone else&#39;s code and wondered why they didn&#39;t just write \u003Ccode>target[prop]\u003C\u002Fcode>.\u003C\u002Fp>",{"id":160,"html":161,"text":161,"type":143,"level":31},"b6","Contents",{"id":163,"type":149,"items":164,"ordered":18},"b7",[165,166,167,168,169,170,171,172,173,174,175,176],"\u003Ca href=\"#why-javascript-proxy-exists\">Why JavaScript Proxy exists\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-a-checkpoint-in-front-of-every-operation\">The mental model: a checkpoint in front of every operation\u003C\u002Fa>","\u003Ca href=\"#stage-1-your-first-proxy--a-get-and-set-trap\">Stage 1: your first proxy — a get and set trap\u003C\u002Fa>","\u003Ca href=\"#stage-2-validation-without-a-setter-for-every-field\">Stage 2: validation without a setter for every field\u003C\u002Fa>","\u003Ca href=\"#stage-3-reflect-and-why-the-receiver-matters\">Stage 3: Reflect, and why the receiver matters\u003C\u002Fa>","\u003Ca href=\"#stage-4-hiding-and-protecting-properties\">Stage 4: hiding and protecting properties\u003C\u002Fa>","\u003Ca href=\"#stage-5-building-a-tiny-reactive-system\">Stage 5: building a tiny reactive system\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-gotchas\">Edge cases and gotchas\u003C\u002Fa>","\u003Ca href=\"#best-practices-when-not-to-reach-for-proxy\">Best practices\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>","\u003Ca href=\"#key-takeaways\">Key takeaways\u003C\u002Fa>",{"id":178,"html":179,"text":179,"type":143,"level":31},"b8","Why JavaScript Proxy exists",{"id":181,"html":182,"type":138},"b9","\u003Cp>Here&#39;s the naive fix for &quot;validate this field whenever it&#39;s set&quot; — a hand-written getter\u002Fsetter pair:\u003C\u002Fp>",{"id":184,"code":185,"type":186,"language":187,"highlight":188},"b10","\u002F\u002F the wrong way — one getter\u002Fsetter pair per field, and it doesn't scale\nclass User {\n  #age;\n  constructor(age) {\n    this.age = age;\n  }\n  get age() {\n    return this.#age;\n  }\n  set age(value) {\n    if (typeof value !== \"number\" || value \u003C 0) {\n      throw new TypeError(\"age must be a non-negative number\");\n    }\n    this.#age = value;\n  }\n}\n\nconst user = new User(30);\nuser.age = -5; \u002F\u002F ✅ correctly throws","code","js",[],{"id":190,"html":191,"type":138},"b11","\u003Cp>This works — for \u003Ccode>age\u003C\u002Fcode>. Add \u003Ccode>email\u003C\u002Fcode>, \u003Ccode>score\u003C\u002Fcode>, and \u003Ccode>role\u003C\u002Fcode>, and you&#39;re maintaining four nearly identical getter\u002Fsetter 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.\u003C\u002Fp>",{"id":193,"html":194,"type":138},"b12","\u003Cp>What you actually want is a way to say &quot;run this code whenever \u003Cem>any\u003C\u002Fem> property is read or written on this object&quot; — one interception point, not N hand-written pairs. That&#39;s precisely what \u003Ccode>Proxy\u003C\u002Fcode> gives you, and \u003Ccode>Reflect\u003C\u002Fcode> is the toolkit that makes writing traps correctly possible.\u003C\u002Fp>",{"id":196,"html":197,"text":197,"type":143,"level":31},"b13","The mental model: a checkpoint in front of every operation",{"id":199,"html":200,"type":138},"b14","\u003Cp>\u003Cstrong>The mental model:\u003C\u002Fstrong> a \u003Ccode>Proxy\u003C\u002Fcode> is not the object — it&#39;s a stand-in that sits in front of the real object (the \u003Cstrong>target\u003C\u002Fstrong>) and intercepts a fixed set of fundamental operations: reading a property, writing one, checking \u003Ccode>in\u003C\u002Fcode>, deleting, listing keys, and a few others. Each operation you intercept is called a \u003Cstrong>trap\u003C\u002Fstrong>. If you don&#39;t define a trap for an operation, it passes straight through to the target, unchanged — and \u003Ccode>Reflect\u003C\u002Fcode> is how you perform that same &quot;pass it through&quot; behavior explicitly, from inside a trap you \u003Cem>did\u003C\u002Fem> define.\u003C\u002Fp>",{"id":202,"html":203,"type":138},"b15","\u003Cp>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&#39;re not improvising; they&#39;re calling the same official procedure (\u003Ccode>Reflect\u003C\u002Fcode>) that would have run automatically if no inspector were there at all.\u003C\u002Fp>",{"id":205,"code":206,"type":186,"language":187,"highlight":207},"b16","const target = { name: \"Ada\", age: 36 };\nconst proxy = new Proxy(target, {\n  \u002F* traps go here — every operation without one passes straight through to target *\u002F\n});\n\nproxy.name; \u002F\u002F \"Ada\" — no `get` trap defined, so this passes straight through",[],{"id":209,"html":210,"type":138},"b17","\u003Cp>Every stage below is this one idea, applied to a different operation.\u003C\u002Fp>",{"id":212,"html":213,"text":213,"type":143,"level":31},"b18","Stage 1: your first proxy — a get and set trap",{"id":215,"html":216,"type":138},"b19","\u003Cp>The two most common traps intercept reading and writing a property:\u003C\u002Fp>",{"id":218,"code":219,"type":186,"language":187,"highlight":220},"b20","const target = { name: \"Ada\", age: 36 };\n\nconst logged = new Proxy(target, {\n  get(obj, prop) {\n    console.log(`read ${String(prop)}`);\n    return obj[prop];\n  },\n  set(obj, prop, value) {\n    console.log(`write ${String(prop)} = ${value}`);\n    obj[prop] = value;\n    return true; \u002F\u002F required: signals the write succeeded\n  },\n});\n\nlogged.name; \u002F\u002F logs \"read name\", returns \"Ada\"\nlogged.age = 37; \u002F\u002F logs \"write age = 37\"",[],{"id":222,"html":223,"type":224},"b21","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> one \u003Ccode>get\u003C\u002Fcode>\u002F\u003Ccode>set\u003C\u002Fcode> pair intercepts \u003Cem>every\u003C\u002Fem> property on the object, in one place — not one pair per field. The \u003Ccode>set\u003C\u002Fcode> trap must return \u003Ccode>true\u003C\u002Fcode> (or any truthy value); return a falsy value and JavaScript throws a \u003Ccode>TypeError\u003C\u002Fcode>, because the engine treats a falsy return as &quot;this write failed.&quot;\u003C\u002Fp>\n","quote",{"id":226,"html":227,"text":227,"type":143,"level":31},"b22","Stage 2: validation without a setter for every field",{"id":229,"html":230,"type":138},"b23","\u003Cp>Now replace the \u003Ccode>User\u003C\u002Fcode> class&#39;s boilerplate with one reusable \u003Ccode>set\u003C\u002Fcode> trap and a table of rules:\u003C\u002Fp>",{"id":232,"code":233,"type":186,"language":187,"highlight":234},"b24","function validated(target, rules) {\n  return new Proxy(target, {\n    set(obj, prop, value) {\n      const rule = rules[prop];\n      if (rule && !rule(value)) {\n        throw new TypeError(`invalid value for ${String(prop)}: ${value}`);\n      }\n      obj[prop] = value;\n      return true;\n    },\n  });\n}\n\nconst user = validated(\n  { name: \"Ada\", age: 36 },\n  { age: (v) => typeof v === \"number\" && v >= 0 }\n);\n\nuser.age = 37; \u002F\u002F ✅ passes the rule, write proceeds\nuser.age = -5; \u002F\u002F ❌ TypeError: invalid value for age: -5",[],{"id":236,"html":237,"type":138},"b25","\u003Cp>Adding a validated field for \u003Ccode>email\u003C\u002Fcode> or \u003Ccode>score\u003C\u002Fcode> is now a one-line rule in the \u003Ccode>rules\u003C\u002Fcode> object, not a new getter\u002Fsetter pair. The check lives in exactly one place — the \u003Ccode>set\u003C\u002Fcode> trap — no matter how many fields you validate. In TypeScript, \u003Ccode>validated\u003C\u002Fcode> 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 \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Ftypescript-generics-the-complete-guide-with-cheat-sheet-45a9\">guide to TypeScript generics\u003C\u002Fa>.\u003C\u002Fp>",{"id":239,"html":240,"type":138},"b26","\u003C!-- playground:start -->",{"id":242,"html":243,"text":243,"type":143,"level":31},"b27","🎮 Try it yourself",{"id":245,"html":246,"type":138},"b28","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-javascript-proxy-reflect\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":248,"html":249,"type":138},"b29","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":251,"html":252,"type":138},"b30","\u003C!-- playground:end -->",{"id":254,"html":255,"text":255,"type":143,"level":31},"b31","Stage 3: Reflect, and why the receiver matters",{"id":257,"html":258,"type":138},"b32","\u003Cp>Stage 1&#39;s traps forwarded reads and writes with \u003Ccode>obj[prop]\u003C\u002Fcode> directly. That works for plain data, but it quietly breaks once a \u003Cstrong>getter\u003C\u002Fstrong> and a \u003Cstrong>prototype chain\u003C\u002Fstrong> are involved — and this is the exact bug \u003Ccode>Reflect\u003C\u002Fcode> exists to prevent.\u003C\u002Fp>",{"id":260,"code":261,"type":186,"language":187,"highlight":262},"b33","const target = {\n  get self() {\n    return this;\n  },\n};\n\nconst handler = {\n  get(target, prop) {\n    return target[prop]; \u002F\u002F ❌ forwards using `target` as `this`, not the actual receiver\n  },\n};\n\nconst proxy = new Proxy(target, handler);\nconst obj = Object.create(proxy);\n\nconsole.log(obj.self === obj); \u002F\u002F false — `this` inside the getter was bound to `target`",[],{"id":264,"html":265,"type":138},"b34","\u003Cp>\u003Ccode>obj.self\u003C\u002Fcode> should return \u003Ccode>obj\u003C\u002Fcode> — that&#39;s what a getter returning \u003Ccode>this\u003C\u002Fcode> means when you access it \u003Cem>through\u003C\u002Fem> \u003Ccode>obj\u003C\u002Fcode>. But the trap wrote \u003Ccode>target[prop]\u003C\u002Fcode>, so the getter ran with \u003Ccode>this\u003C\u002Fcode> bound to \u003Ccode>target\u003C\u002Fcode>, not \u003Ccode>obj\u003C\u002Fcode>. The fix is to forward the operation with \u003Ccode>Reflect.get\u003C\u002Fcode>, which takes a third argument — the \u003Cstrong>receiver\u003C\u002Fstrong> — and passes it through as \u003Ccode>this\u003C\u002Fcode>:\u003C\u002Fp>",{"id":267,"code":268,"type":186,"language":187,"highlight":269},"b35","const handler2 = {\n  get(target, prop, receiver) {\n    return Reflect.get(target, prop, receiver); \u002F\u002F forwards the real receiver as `this`\n  },\n};\n\nconst proxy2 = new Proxy(target, handler2);\nconst obj2 = Object.create(proxy2);\n\nconsole.log(obj2.self === obj2); \u002F\u002F true — Reflect.get passed `obj2` through as the receiver",[],{"id":271,"html":272,"type":224},"b36","\u003Cp>\u003Cstrong>Key concept:\u003C\u002Fstrong> every trap&#39;s default behavior — what would happen with \u003Cem>no\u003C\u002Fem> trap at all — is exactly what its matching \u003Ccode>Reflect\u003C\u002Fcode> method does. \u003Ccode>target[prop]\u003C\u002Fcode> looks equivalent, but it silently drops the receiver; \u003Ccode>Reflect.get(target, prop, receiver)\u003C\u002Fcode> is the one that actually replicates the engine&#39;s own default.\u003C\u002Fp>\n",{"id":274,"html":275,"type":138},"b37","\u003Cp>\u003Ccode>Reflect\u003C\u002Fcode> isn&#39;t a Proxy-only feature — it mirrors all 13 of the fundamental object operations (\u003Ccode>get\u003C\u002Fcode>, \u003Ccode>set\u003C\u002Fcode>, \u003Ccode>has\u003C\u002Fcode>, \u003Ccode>deleteProperty\u003C\u002Fcode>, \u003Ccode>ownKeys\u003C\u002Fcode>, \u003Ccode>getPrototypeOf\u003C\u002Fcode>, \u003Ccode>setPrototypeOf\u003C\u002Fcode>, \u003Ccode>isExtensible\u003C\u002Fcode>, \u003Ccode>preventExtensions\u003C\u002Fcode>, \u003Ccode>defineProperty\u003C\u002Fcode>, \u003Ccode>getOwnPropertyDescriptor\u003C\u002Fcode>, \u003Ccode>apply\u003C\u002Fcode>, \u003Ccode>construct\u003C\u002Fcode>) as plain functions instead of operators or statements. Outside a Proxy trap, that mostly matters for two things: \u003Ccode>Reflect.ownKeys(obj)\u003C\u002Fcode> gets you every own key (strings \u003Cem>and\u003C\u002Fem> symbols) in one call, and \u003Ccode>Reflect.construct(Ctor, args)\u003C\u002Fcode> calls a constructor with a dynamic argument list without \u003Ccode>new Ctor(...args)\u003C\u002Fcode>&#39;s syntax constraints.\u003C\u002Fp>",{"id":277,"html":278,"text":278,"type":143,"level":31},"b38","Stage 4: hiding and protecting properties",{"id":280,"html":281,"type":138},"b39","\u003Cp>Traps aren&#39;t limited to \u003Ccode>get\u003C\u002Fcode>\u002F\u003Ccode>set\u003C\u002Fcode>. \u003Ccode>has\u003C\u002Fcode> intercepts the \u003Ccode>in\u003C\u002Fcode> operator, \u003Ccode>deleteProperty\u003C\u002Fcode> intercepts \u003Ccode>delete\u003C\u002Fcode>, and \u003Ccode>ownKeys\u003C\u002Fcode> intercepts \u003Ccode>Object.keys\u003C\u002Fcode>, \u003Ccode>for...in\u003C\u002Fcode>, and \u003Ccode>JSON.stringify\u003C\u002Fcode>:\u003C\u002Fp>",{"id":283,"code":284,"type":186,"language":187,"highlight":285},"b40","const secretHandler = {\n  ownKeys(target) {\n    return Reflect.ownKeys(target).filter((k) => k !== \"password\");\n  },\n  getOwnPropertyDescriptor(target, prop) {\n    if (prop === \"password\") return undefined;\n    return Reflect.getOwnPropertyDescriptor(target, prop);\n  },\n  has(target, prop) {\n    return prop === \"password\" ? false : Reflect.has(target, prop);\n  },\n};\n\nconst account = new Proxy({ user: \"ada\", password: \"hunter2\" }, secretHandler);\n\nObject.keys(account); \u002F\u002F [\"user\"]\nJSON.stringify(account); \u002F\u002F '{\"user\":\"ada\"}'\n\"password\" in account; \u002F\u002F false\naccount.password; \u002F\u002F still \"hunter2\" — no `get` trap was defined here",[],{"id":287,"html":288,"type":138},"b41","\u003Cp>That last line matters: hiding a key from enumeration (\u003Ccode>ownKeys\u003C\u002Fcode>\u002F\u003Ccode>has\u003C\u002Fcode>) is a \u003Cem>different\u003C\u002Fem> guarantee from blocking direct access (\u003Ccode>get\u003C\u002Fcode>). This example only hides \u003Ccode>password\u003C\u002Fcode> from listing and serialization — anyone who already knows the key name can still read it. If you want both, add a \u003Ccode>get\u003C\u002Fcode> trap that throws or returns \u003Ccode>undefined\u003C\u002Fcode> for that key.\u003C\u002Fp>",{"id":290,"html":291,"text":291,"type":143,"level":31},"b42","Stage 5: building a tiny reactive system",{"id":293,"html":294,"type":138},"b43","\u003Cp>This is the payoff: the same mechanism that powers Vue 3&#39;s reactivity system (Vue 2 used \u003Ccode>Object.defineProperty\u003C\u002Fcode>; Vue 3&#39;s official migration guide documents the switch to \u003Ccode>Proxy\u003C\u002Fcode>), stripped to its essence — a \u003Ccode>set\u003C\u002Fcode> trap that notifies subscribers whenever a value actually changes:\u003C\u002Fp>",{"id":296,"code":297,"type":186,"language":187,"highlight":298},"b44","function reactive(obj) {\n  const subscribers = new Set();\n  const proxy = new Proxy(obj, {\n    get(target, prop, receiver) {\n      return Reflect.get(target, prop, receiver);\n    },\n    set(target, prop, value, receiver) {\n      const changed = target[prop] !== value;\n      const result = Reflect.set(target, prop, value, receiver);\n      if (changed) subscribers.forEach((fn) => fn(prop, value));\n      return result;\n    },\n  });\n  return { proxy, subscribe: (fn) => subscribers.add(fn) };\n}\n\nconst { proxy: state, subscribe } = reactive({ count: 0 });\nsubscribe((prop, value) => console.log(`${prop} changed to ${value}`));\n\nstate.count++; \u002F\u002F logs \"count changed to 1\" — a plain increment triggered the subscriber",[],{"id":300,"html":301,"type":138},"b45","\u003Cp>No \u003Ccode>state.setCount(...)\u003C\u002Fcode> call, no manual &quot;mark dirty&quot; step — \u003Ccode>state.count++\u003C\u002Fcode> is ordinary JavaScript, and the \u003Ccode>set\u003C\u002Fcode> trap is where the framework hooks in to schedule a re-render. This is also why reactive frameworks generally avoid diffing entire objects on every render: the proxy already knows \u003Cem>exactly\u003C\u002Fem> which property changed, the moment it changes.\u003C\u002Fp>",{"id":303,"html":304,"text":304,"type":143,"level":31},"b46","Edge cases and gotchas",{"id":306,"type":149,"items":307,"ordered":18},"b47",[308,309,310,311,312,313],"\u003Cstrong>Identity is not preserved.\u003C\u002Fstrong> \u003Ccode>proxy !== target\u003C\u002Fcode>. If other code holds a reference to the raw \u003Ccode>target\u003C\u002Fcode> and compares it with \u003Ccode>===\u003C\u002Fcode> against the \u003Ccode>proxy\u003C\u002Fcode>, or stores one in a \u003Ccode>Set\u003C\u002Fcode>\u002F\u003Ccode>Map\u003C\u002Fcode> and looks up the other, the comparison fails. Always thread the proxy through consistently — don&#39;t mix references to the target and the proxy for the same logical object.","\u003Cstrong>\u003Ccode>Map\u003C\u002Fcode> and \u003Ccode>Set\u003C\u002Fcode> can&#39;t be proxied directly.\u003C\u002Fstrong> Wrapping a real \u003Ccode>Map\u003C\u002Fcode> or \u003Ccode>Set\u003C\u002Fcode> in a \u003Ccode>Proxy\u003C\u002Fcode> and calling \u003Ccode>.get()\u003C\u002Fcode>\u002F\u003Ccode>.set()\u003C\u002Fcode>\u002F\u003Ccode>.add()\u003C\u002Fcode> on the proxy throws a \u003Ccode>TypeError\u003C\u002Fcode> (&quot;Method Map.prototype.get called on incompatible receiver&quot;), because those methods depend on an internal slot that only exists on genuine \u003Ccode>Map\u003C\u002Fcode>\u002F\u003Ccode>Set\u003C\u002Fcode> instances — a \u003Ccode>Proxy\u003C\u002Fcode> is a different kind of exotic object and doesn&#39;t have it. If you need to intercept a \u003Ccode>Map\u003C\u002Fcode>, wrap the \u003Cem>methods\u003C\u002Fem> explicitly rather than proxying the instance.","\u003Cstrong>Invariants are enforced by the engine, not by you.\u003C\u002Fstrong> If \u003Ccode>target\u003C\u002Fcode> has a non-configurable, non-writable own property, a \u003Ccode>get\u003C\u002Fcode> trap that returns anything other than the real value throws a \u003Ccode>TypeError\u003C\u002Fcode> — you cannot lie about a property the engine considers frozen. Similarly, \u003Ccode>ownKeys\u003C\u002Fcode> must include every non-configurable own key of \u003Ccode>target\u003C\u002Fcode> or the call throws.","\u003Cstrong>Destructuring a method loses the receiver, same as any object.\u003C\u002Fstrong> \u003Ccode>const { subscribe } = state;\u003C\u002Fcode> then calling \u003Ccode>subscribe()\u003C\u002Fcode> alone runs with \u003Ccode>this\u003C\u002Fcode> as \u003Ccode>undefined\u003C\u002Fcode> in strict mode — this isn&#39;t Proxy-specific, but it&#39;s easy to trip over once you&#39;ve wrapped an object in traps and assume the wrapping changes calling conventions. It doesn&#39;t.","\u003Cstrong>Revocable proxies exist for exactly one purpose: capability revocation.\u003C\u002Fstrong> \u003Ccode>const { proxy, revoke } = Proxy.revocable(target, handler);\u003C\u002Fcode> gives you a proxy you can permanently disable later — after \u003Ccode>revoke()\u003C\u002Fcode>, every operation on \u003Ccode>proxy\u003C\u002Fcode> throws. Useful for handing out a reference that must stop working once a session ends or a component unmounts, without tracking down every place that reference was passed.","\u003Cstrong>Every fundamental operation becomes a function call.\u003C\u002Fstrong> A \u003Ccode>get\u003C\u002Fcode>\u002F\u003Ccode>set\u003C\u002Fcode> trap runs real JavaScript on every property access, which is measurably slower than a plain object for extremely hot loops touching millions of properties. This rarely matters for UI state or validation layers; it does matter if you&#39;re tempted to proxy a tight numerical loop.",{"id":315,"html":316,"text":316,"type":143,"level":31},"b48","Best practices: when (not) to reach for Proxy",{"id":318,"html":319,"type":138},"b49","\u003Cp>\u003Cstrong>Reach for a Proxy when\u003C\u002Fstrong> the behavior is cross-cutting — it applies to every property, not one: validation layers, reactive state, logging\u002Finstrumentation, lazy-loading a relation the first time it&#39;s touched, or sandboxing a reference you may need to revoke later.\u003C\u002Fp>",{"id":321,"html":322,"type":138},"b50","\u003Cp>\u003Cstrong>Don&#39;t reach for a Proxy when\u003C\u002Fstrong> a single field needs a single check — a plain getter\u002Fsetter pair on a class is clearer and faster for that one case. Reserve \u003Ccode>Proxy\u003C\u002Fcode> for when you&#39;d otherwise be copy-pasting the same trap logic across several fields.\u003C\u002Fp>",{"id":324,"html":325,"type":138},"b51","\u003Cp>\u003Cstrong>Don&#39;t reach for a Proxy to copy or clone data.\u003C\u002Fstrong> A \u003Ccode>Proxy\u003C\u002Fcode> intercepts operations on the \u003Cem>original\u003C\u002Fem> object — it is not a copy. If what you actually need is an independent snapshot of an object&#39;s current data, that&#39;s \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fyouve-been-deep-cloning-objects-with-a-json-hack-structuredclone-does-it-right-2j0f\">\u003Ccode>structuredClone\u003C\u002Fcode>\u003C\u002Fa>, not a Proxy — the two solve opposite problems and are easy to reach for interchangeably by mistake.\u003C\u002Fp>",{"id":327,"html":328,"type":138},"b52","\u003Cp>\u003Cstrong>Don&#39;t proxy built-ins directly.\u003C\u002Fstrong> As the gotchas above show, \u003Ccode>Map\u003C\u002Fcode>, \u003Ccode>Set\u003C\u002Fcode>, \u003Ccode>Date\u003C\u002Fcode>, and similar built-ins carry internal slots a \u003Ccode>Proxy\u003C\u002Fcode> can&#39;t forward. Wrap the specific methods you need instead of proxying the instance.\u003C\u002Fp>",{"id":330,"html":331,"text":331,"type":143,"level":31},"b53","FAQ",{"id":333,"html":334,"text":334,"type":143,"level":43},"b54","What is the difference between Object.defineProperty and Proxy?",{"id":336,"html":337,"type":138},"b55","\u003Cp>\u003Ccode>Object.defineProperty\u003C\u002Fcode> configures one property on one object at a time — you call it once per field you want to intercept. A \u003Ccode>Proxy\u003C\u002Fcode> wraps the \u003Cem>entire\u003C\u002Fem> object with a single set of traps that apply to every property, including ones added later, which is why Vue 3 moved from the former to the latter.\u003C\u002Fp>",{"id":339,"html":340,"text":340,"type":143,"level":43},"b56","Does Reflect replace Proxy?",{"id":342,"html":343,"type":138},"b57","\u003Cp>No — they&#39;re complementary, not alternatives. \u003Ccode>Proxy\u003C\u002Fcode> is how you intercept an operation; \u003Ccode>Reflect\u003C\u002Fcode> is how you correctly perform that operation&#39;s default behavior (including forwarding the receiver) from inside the trap you wrote.\u003C\u002Fp>",{"id":345,"html":346,"text":346,"type":143,"level":43},"b58","Can I use Proxy on an array?",{"id":348,"html":349,"type":138},"b59","\u003Cp>Yes. Array index access, \u003Ccode>length\u003C\u002Fcode>, and methods like \u003Ccode>push\u003C\u002Fcode> all go through the same \u003Ccode>get\u003C\u002Fcode>\u002F\u003Ccode>set\u003C\u002Fcode> traps (array indices are just string-keyed properties under the hood). A \u003Ccode>set\u003C\u002Fcode> trap on an array proxy fires once per element write, including the implicit \u003Ccode>length\u003C\u002Fcode> update that array mutation methods perform.\u003C\u002Fp>",{"id":351,"html":352,"text":352,"type":143,"level":43},"b60","Can I proxy a Map or a Set?",{"id":354,"html":355,"type":138},"b61","\u003Cp>Not directly — see the gotchas section above. Calling a \u003Ccode>Map\u003C\u002Fcode>\u002F\u003Ccode>Set\u003C\u002Fcode> method on a \u003Ccode>Proxy\u003C\u002Fcode> wrapping one throws a \u003Ccode>TypeError\u003C\u002Fcode>, because those methods require an internal slot only real \u003Ccode>Map\u003C\u002Fcode>\u002F\u003Ccode>Set\u003C\u002Fcode> instances have.\u003C\u002Fp>",{"id":357,"html":358,"text":358,"type":143,"level":43},"b62","Is a Proxy the same type as its target?",{"id":360,"html":361,"type":138},"b63","\u003Cp>\u003Ccode>typeof proxy\u003C\u002Fcode> matches \u003Ccode>typeof target\u003C\u002Fcode> (both \u003Ccode>&quot;object&quot;\u003C\u002Fcode>, or \u003Ccode>&quot;function&quot;\u003C\u002Fcode> if the target is callable and you defined \u003Ccode>apply\u003C\u002Fcode>\u002F\u003Ccode>construct\u003C\u002Fcode> traps), and \u003Ccode>instanceof\u003C\u002Fcode> checks pass through correctly. But \u003Ccode>proxy !== target\u003C\u002Fcode> — they are not the same reference, which matters for equality checks and collection membership.\u003C\u002Fp>",{"id":363,"html":364,"text":364,"type":143,"level":43},"b64","Does JSON.stringify work on a Proxy?",{"id":366,"html":367,"type":138},"b65","\u003Cp>Yes, and it respects your traps: \u003Ccode>JSON.stringify\u003C\u002Fcode> reads properties through \u003Ccode>ownKeys\u003C\u002Fcode>, \u003Ccode>getOwnPropertyDescriptor\u003C\u002Fcode>, and \u003Ccode>get\u003C\u002Fcode>, in that order, so a proxy that hides or transforms properties in those traps produces correspondingly different JSON — exactly as shown in Stage 4.\u003C\u002Fp>",{"id":369,"html":370,"text":370,"type":143,"level":31},"b66","Cheat sheet",{"id":372,"head":373,"rows":378,"type":423},"b67",[374,375,376,377],"Trap","Intercepts","Matching Reflect call","Notes",[379,384,389,394,399,404,409,414,419],[380,381,382,383],"\u003Ccode>get\u003C\u002Fcode>","\u003Ccode>obj.prop\u003C\u002Fcode>, \u003Ccode>obj[prop]\u003C\u002Fcode>","\u003Ccode>Reflect.get(t, p, r)\u003C\u002Fcode>","Must return target&#39;s real value for non-configurable, non-writable props",[385,386,387,388],"\u003Ccode>set\u003C\u002Fcode>","\u003Ccode>obj.prop = v\u003C\u002Fcode>","\u003Ccode>Reflect.set(t, p, v, r)\u003C\u002Fcode>","Must return \u003Ccode>true\u003C\u002Fcode>\u002Ftruthy or a \u003Ccode>TypeError\u003C\u002Fcode> is thrown",[390,391,392,393],"\u003Ccode>has\u003C\u002Fcode>","\u003Ccode>&quot;prop&quot; in obj\u003C\u002Fcode>","\u003Ccode>Reflect.has(t, p)\u003C\u002Fcode>","Doesn&#39;t block reads — combine with \u003Ccode>get\u003C\u002Fcode> to fully hide a key",[395,396,397,398],"\u003Ccode>deleteProperty\u003C\u002Fcode>","\u003Ccode>delete obj.prop\u003C\u002Fcode>","\u003Ccode>Reflect.deleteProperty(t, p)\u003C\u002Fcode>","Return \u003Ccode>false\u003C\u002Fcode> to reject the delete",[400,401,402,403],"\u003Ccode>ownKeys\u003C\u002Fcode>","\u003Ccode>Object.keys\u003C\u002Fcode>, \u003Ccode>for...in\u003C\u002Fcode>, \u003Ccode>JSON.stringify\u003C\u002Fcode>","\u003Ccode>Reflect.ownKeys(t)\u003C\u002Fcode>","Must include every non-configurable own key",[405,406,407,408],"\u003Ccode>getOwnPropertyDescriptor\u003C\u002Fcode>","\u003Ccode>Object.getOwnPropertyDescriptor\u003C\u002Fcode>","\u003Ccode>Reflect.getOwnPropertyDescriptor(t, p)\u003C\u002Fcode>","Pair with \u003Ccode>ownKeys\u003C\u002Fcode> when hiding a key",[410,411,412,413],"\u003Ccode>apply\u003C\u002Fcode>","calling the proxy as a function","\u003Ccode>Reflect.apply(fn, this, args)\u003C\u002Fcode>","Only relevant if \u003Ccode>target\u003C\u002Fcode> is callable",[415,416,417,418],"\u003Ccode>construct\u003C\u002Fcode>","\u003Ccode>new proxy(...)\u003C\u002Fcode>","\u003Ccode>Reflect.construct(Ctor, args)\u003C\u002Fcode>","Only relevant if \u003Ccode>target\u003C\u002Fcode> is a constructor",[420,421,421,422],"\u003Ccode>Proxy.revocable(t, h)\u003C\u002Fcode>","—","Returns \u003Ccode>{ proxy, revoke }\u003C\u002Fcode>; \u003Ccode>revoke()\u003C\u002Fcode> disables the proxy permanently","table",{"id":425,"code":426,"type":186,"language":187,"highlight":427},"b68","\u002F\u002F The whole pattern, copy-paste ready: validated + reactive state, correctly forwarded\nfunction reactiveValidated(obj, rules = {}) {\n  const subscribers = new Set();\n  const proxy = new Proxy(obj, {\n    get(target, prop, receiver) {\n      return Reflect.get(target, prop, receiver); \u002F\u002F always forward the receiver\n    },\n    set(target, prop, value, receiver) {\n      const rule = rules[prop];\n      if (rule && !rule(value)) {\n        throw new TypeError(`invalid value for ${String(prop)}: ${value}`);\n      }\n      const changed = target[prop] !== value;\n      const result = Reflect.set(target, prop, value, receiver);\n      if (changed) subscribers.forEach((fn) => fn(prop, value));\n      return result; \u002F\u002F must be truthy, or JS throws for you\n    },\n  });\n  return { proxy, subscribe: (fn) => subscribers.add(fn) };\n}\n\nconst { proxy: state, subscribe } = reactiveValidated(\n  { age: 30 },\n  { age: (v) => typeof v === \"number\" && v >= 0 }\n);\nsubscribe((prop, value) => console.log(`${prop} -> ${value}`));\n\nstate.age = 31; \u002F\u002F ✅ logs \"age -> 31\"\nstate.age = -1; \u002F\u002F ❌ throws before the subscriber ever runs",[],{"id":429,"html":430,"type":138},"b69","\u003C!-- quiz:start -->",{"id":432,"html":433,"text":433,"type":143,"level":31},"b70","🧠 Test yourself",{"id":435,"html":436,"type":138},"b71","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-javascript-proxy-reflect\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":438,"html":439,"type":138},"b72","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":441,"html":442,"type":138},"b73","\u003C!-- quiz:end -->",{"id":444,"html":445,"text":445,"type":143,"level":31},"b74","Key takeaways",{"id":447,"type":149,"items":448,"ordered":18},"b75",[449,450,451,452,453],"A \u003Ccode>Proxy\u003C\u002Fcode> intercepts fundamental operations on an object — it is a stand-in in front of the target, not a copy of it, and \u003Ccode>proxy !== target\u003C\u002Fcode>.","Every trap has a matching \u003Ccode>Reflect\u003C\u002Fcode> method that performs that operation&#39;s true default behavior, including forwarding the receiver — use \u003Ccode>Reflect.get(target, prop, receiver)\u003C\u002Fcode>, not \u003Ccode>target[prop]\u003C\u002Fcode>, inside a trap.","Hiding a property from enumeration (\u003Ccode>ownKeys\u003C\u002Fcode>\u002F\u003Ccode>has\u003C\u002Fcode>) and blocking direct access (\u003Ccode>get\u003C\u002Fcode>) are separate guarantees — combine the traps you actually need.","\u003Ccode>Map\u003C\u002Fcode>, \u003Ccode>Set\u003C\u002Fcode>, and similar built-ins can&#39;t be proxied directly because their methods depend on internal slots a \u003Ccode>Proxy\u003C\u002Fcode> doesn&#39;t carry.","Reach for \u003Ccode>Proxy\u003C\u002Fcode> when behavior is cross-cutting across every property (validation, reactivity, logging); reach for a plain getter\u002Fsetter, or \u003Ccode>structuredClone\u003C\u002Fcode> for copies, when it isn&#39;t.",{"id":455,"html":456,"type":138},"b76","\u003Cp>That silent \u003Ccode>-5\u003C\u002Fcode> from the top of this article never had a chance to happen in Stage 2 — one \u003Ccode>set\u003C\u002Fcode> trap rejected it before it ever reached the object. You now have the mechanism behind it: a checkpoint in front of the object, \u003Ccode>Reflect\u003C\u002Fcode> to forward what you don&#39;t intercept, and enough of the gotchas to avoid the ones that catch people in production. Where&#39;s the first validation class or manual &quot;notify on change&quot; pattern in your own code that a five-line Proxy could replace? Tell me in the comments.\u003C\u002Fp>",{"id":458,"type":459},"b77","divider",{"id":461,"html":462,"type":138},"b78","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":464,"type":149,"items":465,"ordered":18},"b79",[466,467,468,469,470],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","💼 \u003Cstrong>LinkedIn\u003C\u002Fstrong> — \u003Ca href=\"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F\">linkedin.com\u002Fin\u002Fparsa-jiravand\u003C\u002Fa>","✉️ \u003Cstrong>Email\u003C\u002Fstrong> (work &amp; contract inquiries): \u003Ca href=\"mailto:bestpractice2026@gmail.com\">bestpractice2026@gmail.com\u003C\u002Fa>","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`.\n\n## What you'll learn\n\nBy the end of this guide you'll be able to:\n\n- Explain what a `Proxy` actually is — a stand-in that intercepts operations on an object, not a copy or a wrapper class\n- Write `get`, `set`, `has`, `deleteProperty`, and `ownKeys` traps to validate, hide, and log property access\n- Use `Reflect` correctly, and explain the one bug it exists to prevent\n- Build a small reactive-state system — the same mechanism Vue 3 uses under the hood\n- Recognize the invariants, gotchas, and performance tradeoffs that catch people in production\n\n**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]`.\n\n## Contents\n\n- [Why JavaScript Proxy exists](#why-javascript-proxy-exists)\n- [The mental model: a checkpoint in front of every operation](#the-mental-model-a-checkpoint-in-front-of-every-operation)\n- [Stage 1: your first proxy — a get and set trap](#stage-1-your-first-proxy--a-get-and-set-trap)\n- [Stage 2: validation without a setter for every field](#stage-2-validation-without-a-setter-for-every-field)\n- [Stage 3: Reflect, and why the receiver matters](#stage-3-reflect-and-why-the-receiver-matters)\n- [Stage 4: hiding and protecting properties](#stage-4-hiding-and-protecting-properties)\n- [Stage 5: building a tiny reactive system](#stage-5-building-a-tiny-reactive-system)\n- [Edge cases and gotchas](#edge-cases-and-gotchas)\n- [Best practices](#best-practices-when-not-to-reach-for-proxy)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n- [Key takeaways](#key-takeaways)\n\n## Why JavaScript Proxy exists\n\nHere's the naive fix for \"validate this field whenever it's set\" — a hand-written getter\u002Fsetter pair:\n\n```js\n\u002F\u002F the wrong way — one getter\u002Fsetter pair per field, and it doesn't scale\nclass User {\n  #age;\n  constructor(age) {\n    this.age = age;\n  }\n  get age() {\n    return this.#age;\n  }\n  set age(value) {\n    if (typeof value !== \"number\" || value \u003C 0) {\n      throw new TypeError(\"age must be a non-negative number\");\n    }\n    this.#age = value;\n  }\n}\n\nconst user = new User(30);\nuser.age = -5; \u002F\u002F ✅ correctly throws\n```\n\nThis works — for `age`. Add `email`, `score`, and `role`, and you're maintaining four nearly identical getter\u002Fsetter 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.\n\nWhat 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.\n\n## The mental model: a checkpoint in front of every operation\n\n**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.\n\nThink 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.\n\n```js\nconst target = { name: \"Ada\", age: 36 };\nconst proxy = new Proxy(target, {\n  \u002F* traps go here — every operation without one passes straight through to target *\u002F\n});\n\nproxy.name; \u002F\u002F \"Ada\" — no `get` trap defined, so this passes straight through\n```\n\nEvery stage below is this one idea, applied to a different operation.\n\n## Stage 1: your first proxy — a get and set trap\n\nThe two most common traps intercept reading and writing a property:\n\n```js\nconst target = { name: \"Ada\", age: 36 };\n\nconst logged = new Proxy(target, {\n  get(obj, prop) {\n    console.log(`read ${String(prop)}`);\n    return obj[prop];\n  },\n  set(obj, prop, value) {\n    console.log(`write ${String(prop)} = ${value}`);\n    obj[prop] = value;\n    return true; \u002F\u002F required: signals the write succeeded\n  },\n});\n\nlogged.name; \u002F\u002F logs \"read name\", returns \"Ada\"\nlogged.age = 37; \u002F\u002F logs \"write age = 37\"\n```\n\n> **Key concept:** one `get`\u002F`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.\"\n\n## Stage 2: validation without a setter for every field\n\nNow replace the `User` class's boilerplate with one reusable `set` trap and a table of rules:\n\n```js\nfunction validated(target, rules) {\n  return new Proxy(target, {\n    set(obj, prop, value) {\n      const rule = rules[prop];\n      if (rule && !rule(value)) {\n        throw new TypeError(`invalid value for ${String(prop)}: ${value}`);\n      }\n      obj[prop] = value;\n      return true;\n    },\n  });\n}\n\nconst user = validated(\n  { name: \"Ada\", age: 36 },\n  { age: (v) => typeof v === \"number\" && v >= 0 }\n);\n\nuser.age = 37; \u002F\u002F ✅ passes the rule, write proceeds\nuser.age = -5; \u002F\u002F ❌ TypeError: invalid value for age: -5\n```\n\nAdding a validated field for `email` or `score` is now a one-line rule in the `rules` object, not a new getter\u002Fsetter 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](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Ftypescript-generics-the-complete-guide-with-cheat-sheet-45a9).\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-javascript-proxy-reflect\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Stage 3: Reflect, and why the receiver matters\n\nStage 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.\n\n```js\nconst target = {\n  get self() {\n    return this;\n  },\n};\n\nconst handler = {\n  get(target, prop) {\n    return target[prop]; \u002F\u002F ❌ forwards using `target` as `this`, not the actual receiver\n  },\n};\n\nconst proxy = new Proxy(target, handler);\nconst obj = Object.create(proxy);\n\nconsole.log(obj.self === obj); \u002F\u002F false — `this` inside the getter was bound to `target`\n```\n\n`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`:\n\n```js\nconst handler2 = {\n  get(target, prop, receiver) {\n    return Reflect.get(target, prop, receiver); \u002F\u002F forwards the real receiver as `this`\n  },\n};\n\nconst proxy2 = new Proxy(target, handler2);\nconst obj2 = Object.create(proxy2);\n\nconsole.log(obj2.self === obj2); \u002F\u002F true — Reflect.get passed `obj2` through as the receiver\n```\n\n> **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.\n\n`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.\n\n## Stage 4: hiding and protecting properties\n\nTraps aren't limited to `get`\u002F`set`. `has` intercepts the `in` operator, `deleteProperty` intercepts `delete`, and `ownKeys` intercepts `Object.keys`, `for...in`, and `JSON.stringify`:\n\n```js\nconst secretHandler = {\n  ownKeys(target) {\n    return Reflect.ownKeys(target).filter((k) => k !== \"password\");\n  },\n  getOwnPropertyDescriptor(target, prop) {\n    if (prop === \"password\") return undefined;\n    return Reflect.getOwnPropertyDescriptor(target, prop);\n  },\n  has(target, prop) {\n    return prop === \"password\" ? false : Reflect.has(target, prop);\n  },\n};\n\nconst account = new Proxy({ user: \"ada\", password: \"hunter2\" }, secretHandler);\n\nObject.keys(account); \u002F\u002F [\"user\"]\nJSON.stringify(account); \u002F\u002F '{\"user\":\"ada\"}'\n\"password\" in account; \u002F\u002F false\naccount.password; \u002F\u002F still \"hunter2\" — no `get` trap was defined here\n```\n\nThat last line matters: hiding a key from enumeration (`ownKeys`\u002F`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.\n\n## Stage 5: building a tiny reactive system\n\nThis is the payoff: the same mechanism that powers Vue 3's reactivity system (Vue 2 used `Object.defineProperty`; Vue 3's official migration guide documents the switch to `Proxy`), stripped to its essence — a `set` trap that notifies subscribers whenever a value actually changes:\n\n```js\nfunction reactive(obj) {\n  const subscribers = new Set();\n  const proxy = new Proxy(obj, {\n    get(target, prop, receiver) {\n      return Reflect.get(target, prop, receiver);\n    },\n    set(target, prop, value, receiver) {\n      const changed = target[prop] !== value;\n      const result = Reflect.set(target, prop, value, receiver);\n      if (changed) subscribers.forEach((fn) => fn(prop, value));\n      return result;\n    },\n  });\n  return { proxy, subscribe: (fn) => subscribers.add(fn) };\n}\n\nconst { proxy: state, subscribe } = reactive({ count: 0 });\nsubscribe((prop, value) => console.log(`${prop} changed to ${value}`));\n\nstate.count++; \u002F\u002F logs \"count changed to 1\" — a plain increment triggered the subscriber\n```\n\nNo `state.setCount(...)` call, no manual \"mark dirty\" step — `state.count++` is ordinary JavaScript, and the `set` trap is where the framework hooks in to schedule a re-render. This is also why reactive frameworks generally avoid diffing entire objects on every render: the proxy already knows *exactly* which property changed, the moment it changes.\n\n## Edge cases and gotchas\n\n- **Identity is not preserved.** `proxy !== target`. If other code holds a reference to the raw `target` and compares it with `===` against the `proxy`, or stores one in a `Set`\u002F`Map` and looks up the other, the comparison fails. Always thread the proxy through consistently — don't mix references to the target and the proxy for the same logical object.\n- **`Map` and `Set` can't be proxied directly.** Wrapping a real `Map` or `Set` in a `Proxy` and calling `.get()`\u002F`.set()`\u002F`.add()` on the proxy throws a `TypeError` (\"Method Map.prototype.get called on incompatible receiver\"), because those methods depend on an internal slot that only exists on genuine `Map`\u002F`Set` instances — a `Proxy` is a different kind of exotic object and doesn't have it. If you need to intercept a `Map`, wrap the *methods* explicitly rather than proxying the instance.\n- **Invariants are enforced by the engine, not by you.** If `target` has a non-configurable, non-writable own property, a `get` trap that returns anything other than the real value throws a `TypeError` — you cannot lie about a property the engine considers frozen. Similarly, `ownKeys` must include every non-configurable own key of `target` or the call throws.\n- **Destructuring a method loses the receiver, same as any object.** `const { subscribe } = state;` then calling `subscribe()` alone runs with `this` as `undefined` in strict mode — this isn't Proxy-specific, but it's easy to trip over once you've wrapped an object in traps and assume the wrapping changes calling conventions. It doesn't.\n- **Revocable proxies exist for exactly one purpose: capability revocation.** `const { proxy, revoke } = Proxy.revocable(target, handler);` gives you a proxy you can permanently disable later — after `revoke()`, every operation on `proxy` throws. Useful for handing out a reference that must stop working once a session ends or a component unmounts, without tracking down every place that reference was passed.\n- **Every fundamental operation becomes a function call.** A `get`\u002F`set` trap runs real JavaScript on every property access, which is measurably slower than a plain object for extremely hot loops touching millions of properties. This rarely matters for UI state or validation layers; it does matter if you're tempted to proxy a tight numerical loop.\n\n## Best practices: when (not) to reach for Proxy\n\n**Reach for a Proxy when** the behavior is cross-cutting — it applies to every property, not one: validation layers, reactive state, logging\u002Finstrumentation, lazy-loading a relation the first time it's touched, or sandboxing a reference you may need to revoke later.\n\n**Don't reach for a Proxy when** a single field needs a single check — a plain getter\u002Fsetter pair on a class is clearer and faster for that one case. Reserve `Proxy` for when you'd otherwise be copy-pasting the same trap logic across several fields.\n\n**Don't reach for a Proxy to copy or clone data.** A `Proxy` intercepts operations on the *original* object — it is not a copy. If what you actually need is an independent snapshot of an object's current data, that's [`structuredClone`](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fyouve-been-deep-cloning-objects-with-a-json-hack-structuredclone-does-it-right-2j0f), not a Proxy — the two solve opposite problems and are easy to reach for interchangeably by mistake.\n\n**Don't proxy built-ins directly.** As the gotchas above show, `Map`, `Set`, `Date`, and similar built-ins carry internal slots a `Proxy` can't forward. Wrap the specific methods you need instead of proxying the instance.\n\n## FAQ\n\n### What is the difference between Object.defineProperty and Proxy?\n\n`Object.defineProperty` configures one property on one object at a time — you call it once per field you want to intercept. A `Proxy` wraps the *entire* object with a single set of traps that apply to every property, including ones added later, which is why Vue 3 moved from the former to the latter.\n\n### Does Reflect replace Proxy?\n\nNo — they're complementary, not alternatives. `Proxy` is how you intercept an operation; `Reflect` is how you correctly perform that operation's default behavior (including forwarding the receiver) from inside the trap you wrote.\n\n### Can I use Proxy on an array?\n\nYes. Array index access, `length`, and methods like `push` all go through the same `get`\u002F`set` traps (array indices are just string-keyed properties under the hood). A `set` trap on an array proxy fires once per element write, including the implicit `length` update that array mutation methods perform.\n\n### Can I proxy a Map or a Set?\n\nNot directly — see the gotchas section above. Calling a `Map`\u002F`Set` method on a `Proxy` wrapping one throws a `TypeError`, because those methods require an internal slot only real `Map`\u002F`Set` instances have.\n\n### Is a Proxy the same type as its target?\n\n`typeof proxy` matches `typeof target` (both `\"object\"`, or `\"function\"` if the target is callable and you defined `apply`\u002F`construct` traps), and `instanceof` checks pass through correctly. But `proxy !== target` — they are not the same reference, which matters for equality checks and collection membership.\n\n### Does JSON.stringify work on a Proxy?\n\nYes, and it respects your traps: `JSON.stringify` reads properties through `ownKeys`, `getOwnPropertyDescriptor`, and `get`, in that order, so a proxy that hides or transforms properties in those traps produces correspondingly different JSON — exactly as shown in Stage 4.\n\n## Cheat sheet\n\n| Trap | Intercepts | Matching Reflect call | Notes |\n| --- | --- | --- | --- |\n| `get` | `obj.prop`, `obj[prop]` | `Reflect.get(t, p, r)` | Must return target's real value for non-configurable, non-writable props |\n| `set` | `obj.prop = v` | `Reflect.set(t, p, v, r)` | Must return `true`\u002Ftruthy or a `TypeError` is thrown |\n| `has` | `\"prop\" in obj` | `Reflect.has(t, p)` | Doesn't block reads — combine with `get` to fully hide a key |\n| `deleteProperty` | `delete obj.prop` | `Reflect.deleteProperty(t, p)` | Return `false` to reject the delete |\n| `ownKeys` | `Object.keys`, `for...in`, `JSON.stringify` | `Reflect.ownKeys(t)` | Must include every non-configurable own key |\n| `getOwnPropertyDescriptor` | `Object.getOwnPropertyDescriptor` | `Reflect.getOwnPropertyDescriptor(t, p)` | Pair with `ownKeys` when hiding a key |\n| `apply` | calling the proxy as a function | `Reflect.apply(fn, this, args)` | Only relevant if `target` is callable |\n| `construct` | `new proxy(...)` | `Reflect.construct(Ctor, args)` | Only relevant if `target` is a constructor |\n| `Proxy.revocable(t, h)` | — | — | Returns `{ proxy, revoke }`; `revoke()` disables the proxy permanently |\n\n```js\n\u002F\u002F The whole pattern, copy-paste ready: validated + reactive state, correctly forwarded\nfunction reactiveValidated(obj, rules = {}) {\n  const subscribers = new Set();\n  const proxy = new Proxy(obj, {\n    get(target, prop, receiver) {\n      return Reflect.get(target, prop, receiver); \u002F\u002F always forward the receiver\n    },\n    set(target, prop, value, receiver) {\n      const rule = rules[prop];\n      if (rule && !rule(value)) {\n        throw new TypeError(`invalid value for ${String(prop)}: ${value}`);\n      }\n      const changed = target[prop] !== value;\n      const result = Reflect.set(target, prop, value, receiver);\n      if (changed) subscribers.forEach((fn) => fn(prop, value));\n      return result; \u002F\u002F must be truthy, or JS throws for you\n    },\n  });\n  return { proxy, subscribe: (fn) => subscribers.add(fn) };\n}\n\nconst { proxy: state, subscribe } = reactiveValidated(\n  { age: 30 },\n  { age: (v) => typeof v === \"number\" && v >= 0 }\n);\nsubscribe((prop, value) => console.log(`${prop} -> ${value}`));\n\nstate.age = 31; \u002F\u002F ✅ logs \"age -> 31\"\nstate.age = -1; \u002F\u002F ❌ throws before the subscriber ever runs\n```\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-javascript-proxy-reflect\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n## Key takeaways\n\n- A `Proxy` intercepts fundamental operations on an object — it is a stand-in in front of the target, not a copy of it, and `proxy !== target`.\n- Every trap has a matching `Reflect` method that performs that operation's true default behavior, including forwarding the receiver — use `Reflect.get(target, prop, receiver)`, not `target[prop]`, inside a trap.\n- Hiding a property from enumeration (`ownKeys`\u002F`has`) and blocking direct access (`get`) are separate guarantees — combine the traps you actually need.\n- `Map`, `Set`, and similar built-ins can't be proxied directly because their methods depend on internal slots a `Proxy` doesn't carry.\n- Reach for `Proxy` when behavior is cross-cutting across every property (validation, reactivity, logging); reach for a plain getter\u002Fsetter, or `structuredClone` for copies, when it isn't.\n\nThat silent `-5` from the top of this article never had a chance to happen in Stage 2 — one `set` trap rejected it before it ever reached the object. You now have the mechanism behind it: a checkpoint in front of the object, `Reflect` to forward what you don't intercept, and enough of the gotchas to avoid the ones that catch people in production. Where's the first validation class or manual \"notify on change\" pattern in your own code that a five-line Proxy could replace? Tell me in the comments.\n\n---\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)\n- 💼 **LinkedIn** — [linkedin.com\u002Fin\u002Fparsa-jiravand](https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F)\n- ✉️ **Email** (work & contract inquiries): [bestpractice2026@gmail.com](mailto:bestpractice2026@gmail.com)",{"title":108,"canonical":473,"description":109},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweekly-javascript-proxy-reflect","019fe660-932f-704d-80f6-fdabc1cb6939",{"id":476,"locked":18},"019fe660-9a11-7259-9efa-f3f31628310d",[478],{"id":45,"slug":46,"title":48,"_count":479},{"questions":51},[481],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":483,"questionCount":51},{"questions":51}]