Skip to main content

The Proxy object

Proxy is a wrapper around an object that lets you intercept operations on it. Put simply, Proxy gives you a way to stand between your code and the object so you can control what happens when properties are read, changed or otherwise touched.

Theory

TL;DR

  • new Proxy(target, handler) creates a wrapper around the original object.
  • handler is an object with traps, each of which intercepts one operation.
  • The main traps are get, set, has, deleteProperty, ownKeys, apply and construct.
  • The set and deleteProperty traps must return true, otherwise strict mode throws a TypeError.
  • Reflect holds methods matching every trap and performs the default behaviour.
  • Typical uses: validation, logging, default values, property protection, Vue 3 reactivity, i18n with a fallback.

Quick example

javascript
const user = { name: 'Tim', age: 25 }; const proxy = new Proxy(user, { get(target, prop) { console.log(`Reading property "${prop}"`); return target[prop]; }, set(target, prop, value) { console.log(`Changing "${prop}" to "${value}"`); target[prop] = value; return true; // you must return true, otherwise there will be an error } }); console.log(proxy.name); // Reading property "name" -> "Tim" proxy.age = 26; // Changing "age" to "26"

Syntax

javascript
const proxy = new Proxy(target, handler);
ParameterDescription
targetthe original object the proxy is built over
handleran object with traps that intercept operations

A proxy does not copy the object: every operation eventually reaches target, as long as the trap passes it through. If there is no trap for an operation, it runs exactly as if the Proxy did not exist.

Practical trap examples

Validation on write. The set trap checks the type and refuses to store garbage in the object.

javascript
const user = new Proxy({}, { set(target, prop, value) { if (prop === 'age' && typeof value !== 'number') { throw new TypeError('Age must be a number'); } target[prop] = value; return true; } }); user.age = 30; // ok user.age = 'thirty'; // TypeError

Default values. The get trap returns a stand-in value for properties that do not exist.

javascript
const config = new Proxy({}, { get(target, prop) { return prop in target ? target[prop] : '<not set>'; } }); console.log(config.host); // <not set> config.host = 'localhost'; console.log(config.host); // localhost

Protection from deletion. The deleteProperty trap forbids removing a key field.

javascript
const secure = new Proxy({ id: 1, name: 'Admin' }, { deleteProperty(target, prop) { if (prop === 'id') { console.log('Deletion is forbidden!'); return false; } delete target[prop]; return true; } }); delete secure.name; // gets deleted delete secure.id; // Deletion is forbidden!

Reflect and the default behaviour

Reflect is a set of methods matching all the Proxy traps. It lets you invoke the default behaviour whenever you need it.

javascript
function createLoggedObject(obj) { return new Proxy(obj, { get(target, prop, receiver) { console.log(`Access to property "${prop}"`); return Reflect.get(target, prop, receiver); // the standard behaviour } }); } const user = createLoggedObject({ name: 'Tim' }); console.log(user.name); // Access to property "name"

Without Reflect you would have to write target[prop] by hand, and that can break inheritance and this: a getter on the prototype receives the wrong receiver and reads fields from target instead of from the proxy.

Available traps

TrapWhat it interceptsReflect equivalent
getReading a propertyReflect.get()
setWriting a propertyReflect.set()
hasThe prop in obj checkReflect.has()
deletePropertyDeletion, delete obj[prop]Reflect.deleteProperty()
ownKeysGetting all keys (Object.keys, for...in)Reflect.ownKeys()
getOwnPropertyDescriptorA property descriptorReflect.getOwnPropertyDescriptor()
definePropertyCreating or changing a propertyReflect.defineProperty()
preventExtensionsForbidding new propertiesReflect.preventExtensions()
isExtensibleChecking whether the object is extensibleReflect.isExtensible()
getPrototypeOfGetting the prototypeReflect.getPrototypeOf()
setPrototypeOfChanging the prototypeReflect.setPrototypeOf()
applyCalling a functionReflect.apply()
constructCalling through newReflect.construct()

You can proxy not only a plain object but a function too, and then the apply and construct traps come into play:

javascript
function sum(a, b) { return a + b; } const proxySum = new Proxy(sum, { apply(target, thisArg, args) { console.log(`Calling the function with arguments: ${args}`); return Reflect.apply(target, thisArg, args); } }); console.log(proxySum(2, 3)); // Calling the function with arguments: 2,3 -> 5

Where Proxy is genuinely useful

ScenarioExample of use
Data validationType checking on write
Debugging and loggingIntercepting get and set for analysis
Reactivity (Vue 3)Tracking property changes
Protection and restrictionsForbidding deletion or modification of properties
Localisation and key accessi18n, fallback data
API wrappers"Smart" dynamic objects, a REST API built on the fly for instance

An example of a dynamic structure for i18n: instead of an error or undefined the user sees the key itself, and translators immediately notice what is missing.

javascript
const dictionary = { hello: 'Hello', world: 'world' }; const i18n = new Proxy(dictionary, { get(target, key) { if (key in target) return target[key]; return `[${key}]`; // substitute the key instead of a translation } }); console.log(i18n.hello); // Hello console.log(i18n.missingKey); // [missingKey]

A short summary of the terms:

TermDescription
ProxyAn object that intercepts operations on another object
targetThe original object (the target)
handlerAn object with traps (intercepting methods)
trapA method intercepting a particular operation (get, set and so on)
ReflectLets you invoke the standard default behaviour

Common mistakes

  • Forgetting return true in set or deleteProperty. The trap has to tell the engine that the operation succeeded. In strict mode, and modules are always strict, returning false or undefined leads to a TypeError.
  • Writing target[prop] instead of Reflect.get(target, prop, receiver). The receiver is lost, so getters from the prototype get the wrong this.
  • Assuming the proxy changes the object itself. Traps fire only for accesses that go through the proxy. If a reference to target is still around somewhere, every check is bypassed.
  • Breaking the object invariants. For a non-configurable property the get trap cannot return a different value, and for a non-extensible object ownKeys cannot invent new keys: the engine throws a TypeError.
  • Wrapping everything in a proxy. Every operation goes through your code, so a proxy is noticeably slower than a plain object in a hot loop.
  • Expecting Proxy to be polyfillable. Interception is implemented at the engine level, so there is no substitute for old environments without support.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.