Creating defensive objects with ES6 proxies

This past week I spent an hour debugging an issue that I ultimately tracked down to a silly problem: the property I was referencing didn’t exist on the given object. I had typed request.code and it should have been request.query.code. After sternly lecturing myself for not noticing earlier, a pit formed in my stomach. This is exactly the type of situation that the JavaScript haters point out as why JavaScript sucks.

The haters are, in this case, correct. If I had been using a type-safe language then I would have gotten an error telling me that the property didn’t exist, and thus saved me an hour of my life. This wasn’t the first time that I’d encountered this type of error, and it likely wouldn’t be the last. Each time it happens, I stop and think about ways that I could prevent this type of error from happening, but there has never been a good answer. Until ECMAScript 6.

ECMAScript 5

Whereas ECMAScript 5 did some fantastic things for controlling how you can change existing properties, it did nothing for dealing with properties that don’t exist. You can prevent existing properties from being overwritten (setting writable to false) or deleted (setting configurable to false). You can prevent objects from being assigned new properties (using Object.preventExtensions()) or set all properties to be read-only and not deletable (Object.freeze()).

If you don’t want all the properties to read-only, then you can use Object.seal(). This prevents new properties from being added and existing properties from being removed but otherwise allows properties to behave normally. This is the closest thing in ECMAScript 5 to what I want as its intent is to solidify (“seal”) the interface of a particular object. A sealed object, when used in strict mode, will throw an error when you try to add a new property:

"use strict";

var person = {
    name: "Nicholas"
};

Object.seal(person);

person.age = 20;    // Error!

That works really well to notify you that you’re attempting to change the interface of an object by adding a new property. The missing piece of the puzzle is to throw an error when you attempt to read a property that isn’t part of the interface.

Proxies to the rescue

Proxies have a long and complicated history in ECMAScript 6. An early proposal was implemented by both Firefox and Chrome before TC-39 decided to change proxies in a very dramatic way. The changes were, in my opinion, for the better, as they smoothed out a lot of the rough edges from the original proxies proposal (I did some experimenting with the early proposal1).

The biggest change was the introduction of a target object with which the proxy would interact. Instead of just defining traps for particular types of operations, the new “direct” proxies intercept operations intended for the target object. They do this through a series of methods that correspond to under-cover-operations in ECMAScript. For instance, whenever you read a value from an object property, there is an operation called [[Get]] that the JavaScript engine performs. The [[Get]] operation has built-in behavior that can’t be changed, however, proxies allow you to “trap” the call to [[Get]] and perform your own behavior. Consider the following:

var proxy = new Proxy({ name: "Nicholas" }, {
    get: function(target, property) {
        if (property in target) {
            return target[property];
        } else {
            return 35;
        }
    }
});

console.log(proxy.time);        // 35
console.log(proxy.name);        // "Nicholas"
console.log(proxy.title);       // 35

This proxy uses a new object as its target (the first argument to Proxy()). The second argument is an object that defines the traps you want. The get method corresponds to the [[Get]] operation (all other operations behave as normal so long as they are not trapped). The trap receives the target object as the first argument and the property name as the second. This code checks to see if the property exists on the target object and returns the appropriate value. If the property doesn’t exist on the target, the function intentionally ignores the two arguments and always returns 35. So no matter which non-existent property is accessed, the value 35 is always returned.

Getting defensive

Understanding how to intercept the [[Get]] operation is all that is necessary for creating “defensive” objects. I call them defensive because they behave like a defensive teenager trying to assert their independence of their parents’ view of them (“I am not a child, why do you keep treating me like one?”). The goal is to throw an error whenever a nonexistent property is accessed (“I am not a duck, why do you keep treating me like one?”). This can be accomplished using the get trap and just a bit of code:

function createDefensiveObject(target) {
    
    return new Proxy(target, {
        get: function(target, property) {
            if (property in target) {
                return target[property];
            } else {
                throw new ReferenceError("Property \"" + property + "\" does not exist.");
            }
        }
    });
}

The createDefensiveObject() function accepts a target object and creates a defensive object for it. The proxy has a get trap that checks the property when it’s read. If the property exists on the target object, then the value of the property is returned. If, on the other hand, the property does not exist on the object, then an error is thrown. Here’s an example:

var person = {
    name: "Nicholas"
};

var defensivePerson = createDefensiveObject(person);

console.log(defensivePerson.name);        // "Nicholas"
console.log(defensivePerson.age);         // Error!

Here, the name property works as usual while age throws an error.
Defensive objects allow existing properties to be read, but non-existent properties throw an error when read. However, you can still add new properties without error:

var person = {
    name: "Nicholas"
};

var defensivePerson = createDefensiveObject(person);

console.log(defensivePerson.name);        // "Nicholas"

defensivePerson.age = 13;
console.log(defensivePerson.age);         // 13

So objects retain their ability to mutate unless you do something to change that. Properties can always be added but non-existent properties will throw an error when read rather than just returning undefined.

Standard feature detection techniques still work as usual and without error:

var person = {
    name: "Nicholas"
};

var defensivePerson = createDefensiveObject(person);

console.log("name" in defensivePerson);               // true
console.log(defensivePerson.hasOwnProperty("name"));  // true

console.log("age" in defensivePerson);                // false
console.log(defensivePerson.hasOwnProperty("age"));   // false

You can then truly defend the interface of an object, disallowing additions and erroring when accessing a non-existent property, by using a couple of steps:

var person = {
    name: "Nicholas"
};

Object.preventExtensions(person);

var defensivePerson = createDefensiveObject(person);


defensivePerson.age = 13;                 // Error!
console.log(defensivePerson.age);         // Error!

In this case, defensivePerson throws an error both when you try to read from and write to a non-existent property. This effectively mimics the behavior of type-safe languages that enforce interfaces.

Perhaps the most useful time to use defensive objects is when defining a constructor, as this typically indicates that you have a clearly-defined contract that you want to preserve. For example:

function Person(name) {
    this.name = name;

    return createDefensiveObject(this);
}

var person = new Person("Nicholas");

console.log(person.age);         // Error!

By calling createDefensiveObject() inside of a constructor, you can effectively ensure that all instances of Person are defensive.

Conclusion

JavaScript has come a long way recently, but we still have a ways to go to get the same type of time-saving functionality that type-safe languages boast. ECMAScript 6 proxies provide a great way to start enforcing contracts where necessary. The most useful place is in constructors or ECMAScript 6 classes, but it can also be useful to make other objects defensive as well. The goal of defensive objects is to make errors more obvious, so while they may not be appropriate for all objects, they can definitely help when defining API contracts.

  1. Experimenting with ECMAScript 6 proxies by me (NCZOnline)

Comments

  1. Jan Liška

    Why is there an error in the last code example? As I understood, it should be possible to assign new properties to defensive objects. Or maybe there is a missing Object.preventExtensions() in the constructor?
    person.age = 13; // Error!

  2. Nicholas C. Zakas

    Copy-paste error. Thanks for pointing it out. Fixed now.

  3. David Bruant

    I'm skeptical with such a use of proxies in practice.

    "Properties can always be added but non-existent properties will throw an error when read rather than just returning undefined."
    => This is a very development-time-oriented use case. When do you replace an object by a defensive to find typos? Always?

    var defensivePerson = createDefensiveObject(person);
    suggests a change of the variable name. You may want to consider at least:
    person = createDefensiveObject(person);
    which seems less disruptive.

    I believe this sort of problem is much better solved with TypeScript.

    As shown by the red underlying "age", TypeScript is very much capable of finding properties that aren't expected. It doesn't require to rewrite the code.
    You don't need to use TypeScript code generation. Just set a watcher against your code to let the TS compiler find these errors for you.

  4. Nicholas C. Zakas

    You could certainly use defensive objects as a development-only feature and turn it off in production mode. I haven't yet measured the performance characteristics of proxies, but if there's little to no effect, it might be worth just using them.

    I think this is most useful at API boundaries, places where you know what certain properties will be there any everything else won't.

    I agree, I like TypeScript for this sort of thing, but that requires everyone who's working on or with the library to being using TypeScript, whereas with proxies, as long as the environment supports proxies then the behavior is applied without any additional effort.

  5. Rick Waldron

    Awesome article!

    Another great aspect of Proxy objects is that the returned proxy object doesn't break the target's instance identity and prototype chain.

    Take the following example:


    function breakTheChain(target) {
    return {};
    }

    function Person(name) {
    this.name = name;
    return breakTheChain(this);
    }

    var person = new Person("Nicholas");

    console.log( person instanceof Person ); // false

    But when using your example:


    function Person(name) {
    this.name = name;
    return createDefensiveObject(this);
    }

    var person = new Person("Nicholas");
    console.log( person instanceof Person ); // true
  6. David Bruant

    I agree, I like TypeScript for this sort of thing, but that requires everyone who’s working on or with the library to being using TypeScript (...)


    No. That's the point I was trying to convey (and failed at... but it's been repeatedly at other occasions too, I need to work on how to convey my point).
    Any JavaScript file is a valid TypeScript file. So it's possible to have the TypeScript compiler watching the library/project .js files and do the typechecking on the .js files (no need for .ts files, no need to actually generate files since they'll be identical to the source files. I don't think there is an option to not generate files, but you can still generate them to /tmp or /dev/null).

    This way, you benefit from the typechecking without needing to buy into any TypeScript specific extension.

    What I suggesting is just to take advantage of the typechecking capabilities of the compiler while ignoring all other features. Does that make sense?

  7. Ivan

    I was going to advice using TypeScript, but, apparently, someone beat me to it.

    My experience stems from building complete projects with TS + Angular, using TS in projects where many people use plain JS and experimenting with NodeJS + TS.

    So far - I see only benefits from using it whenever possible. You start using it by simply renaming your .js file to .ts file - and that is it. You already get most of the checking.

    Add a typing file from https://github.com/Definite... - and you get even more. Helped me a lot when I was learning Express. Even with things like Angular - TS sometimes leads me to discover a third or fourth parameter in the function where I rarely used a second one.

    All superset features are completely optional. Don't want to annotate your functions return types? Don't do it. TS will figure it our where it can, but won't bother you with errors otherwise. Want to ditch TS altogether? Easily. It generates JS I would otherwise write by hand - very idiomatic. Most fellow Devs I asked could not tell resulting JS from JS written by hand.

    A big sign for me was when I was finally able to use prototypical class inheritance in my Angular Controllers in production thanks to TS. Between duck-typed interfaces and fat arrow functions you can use today and not worry about browser support - there just no reason not to use it.

    P.S. Sorry for the wall of text. Its just that TS is a tech I really want to stick around, so I get very excited whenever it is discussed.

  8. Sergey

    David, that is definitely makes sense. Would you mind to share a link for a tutorial please?
    However when Proxies will be more widely supported (at least in Chrome and Firefox) I would definitely use the trick which Nicholas showed us.

    Thanks

  9. Vignesh

    Great article!.

    Is there any way we can define a defensive proxy in the parent constructor and carry forward to all the subclasses?.

Understanding JavaScript Promises E-book Cover

Demystify JavaScript promises with the e-book that explains not just concepts, but also real-world uses of promises.

Download the Free E-book!

The community edition of Understanding JavaScript Promises is a free download that arrives in minutes.