
Understanding JavaScript Proxies by Examining on-change Library
Javascript Proxies are a new addition in ES6. It’s a powerful feature that can be used for solving various problems elegantly. We are going to examine and re-create a small utility library by Sindre Sorhus called on-change. The aim is to conceptually understand JavaScript Proxies, and in the process, build something so that the concepts are reinforced.
I have tried to keep things as simple as possible.However, a little familiarity with JavaScript language is expected.
So what does on-change do? It’s a small utility that watches an object or array for changes. Let’s see a code sample to make things clear —
A few things to note —
onChange
is a function that takes two parameters: the object to watch, and the function to run when a change is encountered in the said object. It returns an object.- On line 17, when we set
foo
totrue
, thelogger
function is called. - On line 20, when we set an object that is deeply nested, even within arrays,
logger
is called. That is, it works recursively, so it will even detect if we modify a deep property likewatchedObject.a.b[0].c = true
.
So let’s see how we can use JavaScript Proxies to re-create this utility! But before that, let’s study proxies.
What is a Proxy?
Consider this short code snippet —
const someObject = { prop1: 'Awesome' };console.log(someObject.prop1); // Awesomeconsole.log(someObject.prop2); // undefined
If we do someObject.prop1
we are going to get Awesome
. But if we do someObject.prop2
we are going to get undefined
because prop2
does not exist on someObject
.
Let’s say we want to return a default value every time a non-existent property is accessed. That is, someObject.prop2
should give Oops! This property does not exist
instead of undefined
. How can we achieve this without modifying or adding new properties to someObject
?
Welcome proxies! The Oxford English Dictionary defines a proxy as the authority to represent someone else. That is exactly what a Proxy in JavaScript is. Proxies are part of ES6, and they enable us to intercept operations(such as setting a value or deleting a property) performed on objects. While accessing an object’s property, this is what happens —

When using a Proxy, things change a little bit.

As you can see from the above diagram, Proxy sits between the object and the program, mediating the exchange of values. The Proxy can check the object for a property key, and if it does not exist, it can send its own response too.
Having shown how awesome proxies are, let’s see how we can create them in JavaScript. But before that, a few terms that you should be familiar with —
- target — The object for which we will be making the proxy.
- traps — a fancy term for the operations that we will intercept. For example, accessing a property is called
get
trap. Setting a value to a property is called aset
trap. Deleting a property from an object is calleddeleteProperty
trap. There are many traps. You can see all of them here. - handler — The object which contains all the traps, along with their descriptions.
Creating Proxies
The first thing we need is an object for which we are creating the proxy. Let it be this —
const originalObject = { firstName: 'Arfat', lastName: 'Salman' };
Now, we need to think of what traps we are going to intercept. Right now, we are going to intercept the get
trap. The trap will live in the handler. So let’s create it.
const handler = {
get(target, property, receiver) {
console.log(`GET ${property}`);
return target[property];
}
};
A few things to note —
- The
handler
is a normal object. - The traps are functions (or methods) which are part of the handler. The names of the traps are fixed and predefined.
- The
get
trap receives three parameters: target, property, receiver. - The
target
is the original object for which we created the proxy. - The
property
is the name of the property that is being accessed. - The
receiver
is either the proxy or an object that inherits from the proxy.
Now, we need to combine the handler
and the originalObject
. We do this by using the Proxy
constructor.
const proxiedObject = new Proxy(originalObject, handler);
The entire code should look like —

You can run it in the browser’s console, or keep it in a file and run it using node (version ≥ 7 ). Here’s a sample run —

Now, if you log the firstName
property of the proxiedObject
—
console.log(proxiedObject.firstName);
//=> GET firstName
//=> Arfat
We are going to get two logs. If you get same outputs as above, that means everything was set up correctly.
Now, let’s modify the handler to handle non-existent properties.
const newHandler = {
get(target, property, receiver) {
console.log(`GET ${property}`); if (property in target) {
return target[property];
}return 'Oops! This property does not exist.';
}
};
Focus on the bold parts. We check whether the property exists on the target. If it exists, we return its value. Otherwise, we return Oops! This property does not exist.
.
Now, if you do —
console.log(proxiedObject.thisPropertDoesNotExist);
// => GET thisPropertyDoesNotExist
// => Oops! This property does not exist.
you will not get undefined
but the a custom response string.
It should be noted that for operations which do not have any traps defined, they are passed to the target normally as if the proxy did not exist.
Recreating on-change
With the understanding of how proxies work, we are set to recreate the on-change library. As discussed above, onChange
is a function that takes two parameters: the object to watch, and the function which will be executed on every change in the object. Let’s make a function, then —
const onChange = (objToWatch, onChangeFunction) => { };
It doesn’t do anything much right now.
Let’s state the problem again: We want to run onChangeFunction
whenever objToWatch
is changed, that is, either a property is accessed/retrieved, or a new property is added, or a property is deleted.
It seems clear that we are going to use proxies to intercept operations on the object. So let’s return a proxy in the onChange
function with an empty handler. Since the handler does not specify any traps, all operations are transparently passed to the target object, that is, objToWatch
.
const onChange = (objToWatch, onChangeFunction) => {
const handler = {}; return new Proxy(objToWatch, handler);
};
Let’s focus on “when a property is accessed/retrieved” since we understood the get
trap above. So, if in the get
trap, we call the onChangeFunction
, before returning the property’s value, we should be able to do achieve partly what on-change library does. Let’s code it and see —
const onChange = (objToWatch, onChangeFunction) => {
const handler = {
get(target, property, receiver) {
onChangeFunction(); // Calling our function
return target[property];
}
};return new Proxy(objToWatch, handler);
};
This seems about right. Let’s run it before proceeding further —

So we have accomplished one part of the statement. Let’s now focus on when “a new property is added, or a property is deleted”. Since we’ve already laid the groundwork, we just need to add more traps to accomplish the remaining functionality. The trap for setting a property or modifying its value is set
. Let’s add that in the handle
—
const onChange = (objToWatch, onChangeFunction) => {
const handler = {
get(target, property, receiver) {
onChangeFunction();
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
onChangeFunction();
return Reflect.set(target, property, value);
}
};return new Proxy(objToWatch, handler);
};
set
receives 4 parameters: the extra one is the value that is being set. We are using Reflect
because it gives us a programmatic way of manipulating an object. It’s not that different from obj.name = 'Arfat'
type of property setting. You can read here why it’s better to use Reflect
API. You can read more about Reflect API here.
Since we are using Reflect
API, I’m going to replace target[property]
with its equivalent Reflect function in the get
trap as well.
In a similar way, if we want to intercept deletion of a property, we can do so by the deleteProperty
trap. Let’s add that to the handler as well —
const onChange = (objToWatch, onChangeFunction) => {
const handler = {
get(target, property, receiver) {
onChangeFunction();
return Reflect.get(target, property, receiver);
},
set(target, property, value) {
onChangeFunction();
return Reflect.set(target, property, value);
},
deleteProperty(target, property) {
onChangeFunction();
return Reflect.deleteProperty(target, property);
}
};return new Proxy(objToWatch, handler);
};
Well done. If you now run this code —
const logger = () => console.log('I was called');const obj = { a: 'a' };const proxy = onChange(obj, logger);console.log(proxy.a); // logger called here in get trap
proxy.b = 'b'; // logger called here as well in set trap
delete proxy.a; // logger called here in deleteProperty trap
You are going to see I was called
3 times. That means, we’ve successfully re-created the on-change library.
There is one thing that we haven’t account for though. If you have nested objects in an array, they won’t trigger the logger function. For example, if the array is [1, 2, {a: false}]
and you set array[2].a = true
, the logger function will not be called.
It’s easy to rectify this bug. Instead of returning the value in the get
trap, we will return another Proxy
of the value if the value is an object so that the chain of proxies is never broken on objects.
Let’s add that logic to the get
trap —
get(target, property, receiver) {
onChangeFunction();
const value = Reflect.get(target, property, receiver);
if (typeof value === 'object') {
return new Proxy(value, handler);
}
return value;
}
Now, it will work even with nested objects inside arrays and objects.
Final thoughts
Some things that on-change does differently than our implementation are—
- It does not call
onChangeFunction
in theget
trap. - Instead of
set
trap, on-change interceptsdefineProperty
trap.
With the understanding of proxies, and knowledge of traps, these two would be a trivial addition/modifications. So, I’m leaving them for the reader to add themselves. You can read the source of on-change here, for reference.
One other issue that plagues on-change is this — if you have an array, and you do proxiedArray.sort()
or any other function that heavily modifies the array, the logger
function is going to be executed multiple times. For example, sorting the array [2,3,4,5,6,7,1]
executes logger 12 times. This can be a desired functionality, or not. It depends on the developer.
There is another bug in the on-change library. If you read this issue, you will notice that the get
trap violates something called an “Invariant”. Invariants are constraint put on proxy objects by the Proxy API. These constraints dis-allow illegal operations on objects whose descriptors are set a certain way.
The issue lists a potential solution as well. You can read the references below to gain a deeper understanding of proxies and invariants. And if you have never contributed to open-source before, this could be a great beginning for you to make a pull request, correcting the behaviour. 🙂
There are many other features and caveats of proxies. Read the references to understand them better.
References —
You might also like some more articles that I wrote —
Top JavaScript VSCode Extensions for Faster Development
How to NOT React: Common Anti-Patterns and Gotchas in React
✉️ Subscribe to CodeBurst’s once-weekly Email Blast, 🐦 Follow CodeBurst on Twitter, view 🗺️ The 2018 Web Developer Roadmap, and 🕸️ Learn Full Stack Web Development.