Notes of Maks Nemisj

Experiments with JavaScript

Why getters/setters is a bad idea in JavaScript

UPDATE (15 May 2020) : I see a lot of comments regarding TypeScript and that there is no issue with setters/getters while using static typing. Of course, you can safely use getters/setters in the environment, which guarantees static type check, but this article is about vanilla JavaScript. This article is my opinion why I think this feature shouldn’t arrive to the vanilla JavaScript in the first place.

As you know, getters and setters are already a part of the JavaScript for sometime. They’re widely support in all major browsers even starting at IE8. I don’t think that this concept is wrong in general, but I think it’s not very well suited for JavaScript. It might look like getters and setters are a time saver and simplification of your code, but actually they brings hidden errors which are not obvious from the first look.

How does getters and setters work

First a small recap on what are these things are:
Sometimes it is desirable to allow access to a property that returns a dynamically computed value, or you may want reflect the status of an internal variable without requiring the use of explicit method calls.
To illustrate how they work, let’s look at a person object which has two properties: firstName and lastName, and one computed value fullName.
var obj = {
  firstName: "Maks",
  lastName: "Nemisj"
}
The computed value fullName would return a concatenation of both firstName and lastName.
Object.defineProperty(person, 'fullName', {
  get: function () {
    return this.firstName + ' ' + this.lastName;
  }
});
To get the computed value of fullName there is no more need for awful braces like person.fullName(), but a simple var fullName = person.fullName can be used. The same applies to the setters, you could set a value by using the function:
Object.defineProperty(person, 'fullName', {
  set: function (value) {
    var names = value.split(' ');
    this.firstName = names[0];
    this.lastName = names[1];
  }
});
Usage is just as simple with getter: person.fullName = 'Boris Gorbachev' This will call the function defined above and will split Boris Gorbachev into firstName and lastName.

Where is the problem

You maybe think: “Hey, I like setters and getters, they feel more natural, just like JSON.” You’re right, they do, but let’s step back for a moment and look how would fullName worked before getters and setters? For getting a value we would use something like getFullName() and for setting a value person.setFullName('Maks Nemisj') would be used. And what would happen if the name of the function is misspelled and person.getFullName() is written as person.getFulName()? JavaScript would give an error:
person.getFulName();
       ^
TypeError: undefined is not a function
This error is triggered at the right place and at the right moment. Accessing non existing functions of an object will trigger an error – that’s good. Now let’s see what happens when setter is used with the wrong name?
  person.fulName = 'Boris Gorbachev';
Nothing. Objects are extensible and can have dynamically assigned keys and values, so no error will be thrown in runtime. Such behavior means that errors might be visible somewhere in the user interface, or maybe, when some operation is performed on the wrong value, but not at the moment when the real typo occurred. Tracing errors which should happen in the past but shown in the future of the code flow is “so fun”.

Seal to the rescue

This problem could be partially solved by seal API. Whenever an object is sealed, it can’t be mutated, which means that fulName will try to assign a new key to the person object and it will fail. For some reason, when I was testing this in node.js v4.0, it didn’t worked the way I was expecting. So I doubt this solution. What is even more frustrating is that there is no solution for getters at all. As I already mentioned, objects are extensible and are failsafe, which means accessing a non existing key will not result in any error at all. I wouldn’t bother writing this article if this situation would only apply to the object literals, but after rise of ECMAScript 2015 (ES6) and the ability to define getters and setters within Classes, I’ve decided to blog about the possible pitfalls.

Classes to the masses

I know that currently Classes are not very welcome inside of some JavaScript communities. People are arguing about the need of them in a functional/prototype-based language like JavaScript. However, the fact is that classes are in ECMAScript 2015 (ES6) spec and are going to stay there for a while. For me, Classes are the way to specify well defined APIs between the outside world ( consumers ) of the classes and the internals of the application. It is an abstraction which puts rules down in black and white, and assumes that these rules are not going to change any time soon. Time to improve the person object and make a real class of it ( as real as class can be in JavaScript). Person defines the interface for getting and setting fullName.
class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  getFullName() {
    return this.firstName + ' ' + this.lastName;
  }

  setFullName(value) {
    var names = value.split(' ');
    this.firstName = names[0];
    this.lastName = names[1];
  }
}
Classes define a strict interface description, but getters and setters make it less strict than it should be. We’re already used to the swollen errors when typos occur in keys when working with object literals and with JSON. At least I was hoping that Classes would be more strict and provide better feedback to the developers in that sense. Though this situation is not any different when defining getters and setters on a class. It will not stop others from making typos without any feedback.
class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }

  get fullName() {
    return this.firstName + ' ' + this.lastName;
  }

  set fullName(value) {
    var names = value.split(' ');
    this.firstName = names[0];
    this.lastName = names[1];
  }
}
Executing with a typo, won’t give any error:
var person = new Person('Maks', 'Nemisj');
console.log(person.fulName);
The same non-strict, non-verbose, non-traceable behavior leading to possible errors. After I discovered this, my question was: is there anything to do in order to make classes more strict when using getters and setters? I found out: sure there is, but is this worse it? Adding an extra layer of complexity into code just to use fewer braces? It is also possible not to use getters and setters for API definition and that would solve the issue. Unless you’re a hardcore developer and willing to proceed, there is another solution, described below.

Proxy to the rescue?

Besides setters and getters, ECMAScript 2015 (ES6) also comes with proxy object. Proxies help you to define the delegator method which can be used to perform various actions before real access to the key is performed. Actually, it looks like dynamic getters/setters. Proxy objects can be used to trap any access to the instance of the Class and throw an error if a pre-defined getter or setter was not found in that Class. In order to do this, two actions must be performed:
  • Create list of getters and setters based on the Person prototype.
  • Create Proxy object which will test against these lists.
Let’s implement it. First, to find out what kind of getters and setters are available on the class Person, it’s
possible to use getOwnPropertyNames and getOwnPropertyDescriptor:
var names = Object.getOwnPropertyNames(Person.prototype);

var getters = names.filter((name) => {
  var result =  Object.getOwnPropertyDescriptor(Person.prototype, name);
  return !!result.get;
});

var setters = names.filter((name) => {
  var result =  Object.getOwnPropertyDescriptor(Person.prototype, name);
  return !!result.set;
});
After that, create a Proxy object, which will be tested against these lists:
var handler = {
  get(target, name) {
    if (getters.indexOf(name) != -1) {
      return target[name];
    }
    throw new Error('Getter "' + name + '" not found in "Person"');
  },

  set(target, name) {
    if (setters.indexOf(name) != -1) {
      return target[name];
    }
    throw new Error('Setter "' + name + '" not found in "Person"');
  }
};

person = new Proxy(person, handler);
Now, whenever you will try to access person.fulName, message Error: Getter "fulName" not found in "Person" will be shown. I hope this article helped you to understand the whole picture about getters and setters, and danger which they can bring into the code.

, , ,

21 thoughts on “Why getters/setters is a bad idea in JavaScript

  • Samir Alajmovic says:

    Instead of Object.freeze, you should have used the Object.seal which is the appropiate method to use when you want to be able to change existing properties (freeze doesn’t allow modification to the object anymore), it also throws an error when you attempt to add a new property.

    let a = {fullName: “”};
    Object.seal(a);

    // Succeeds
    a.fullName = “Adam”;

    // Fails.
    a.fulName = “Adam”;

  • Maks Nemisj says:

    @Samir, Thanks for pointing out. Only the problem is that it still allows to change stuff: “Values of present properties can still be changed as long as they are writable.” So it’s not a big difference in the context I explain.

  • Samir Alajmovic says:

    I’m not sure I understand what you mean as it works for setting as in the example of how it was done before with method call, but not for the get as javascript allows you to retrieve undefined properties.

    Object.seal means that you cannot add new properties to the object, only overwrite existing ones but that doesn’t apply when we have get / set as the set function is predefined. Just saying, the Object.freeze method wasn’t even a candidate to begin with since it allows you to retrieve undefined properties of an object, which is what I assume you were trying to safeguard against. To add, I personally don’t use get/set as I don’t mind using the extra parentheses to safeguard against type errors.

    let person = {firstName: “Samir”, lastName: “Alajmovic”};

    Object.defineProperty(person, ‘fullName’, {
    get: function () {
    return this.firstName + ‘ ‘ + this.lastName;
    },
    set: function (value) {
    var names = value.split(‘ ‘);
    this.firstName = names[0];
    this.lastName = names[1];
    }
    });

    Object.seal(person);

    // Works.
    person.fullName = “samir Alajmo”;

    // Uncaught TypeError: Can’t add property fulName, object is not extensible.
    person.fulName = “samir Alajmo”;

    // Silent error.
    let fullName = person.fulName;

  • Maks Nemisj says:

    @Samir, Maybe I misunderstood you. My point is that `freeze` and `seal` are both working the same to the existing properties. Which means it doesn’t matter what to use to get the desired functionality for setters. Does it?

  • Samir Alajmovic says:

    var a = Object.freeze({name: 1});
    var b = Object.seal({name: 1});

    // Throws error.
    a.name = 2;

    // Throws error.
    a.nam = 2;

    // Works.
    b.name = 2;

    // Throws error.
    b.nam = 2;

    With freeze, the object is completely locked down, meaning you cannot add new properties nor edit existing ones. With seal, the object is partially locked down, you cannot add new properties, but you can edit existing ones. It’s this functionality of Object.seal that makes setting an undefined property throw an error and as such gives the intended behavior for setters (but not for getters, since console.log(b.nam) still works).

    I didn’t know about the proxy handler though, so cheers for that one!

  • Maks Nemisj says:

    @Samir, ok, i got it. thanks 🙂 Will change in the article.

  • clem says:

    I often get this problem with canvas 2d context api: it has a lot of setter like fillStyle or strokeStyle, lineCap or globalAlpha

  • Igor Bukanov says:

    Note that one can put creation of the Proxy inside the constructor like in:

    class Person {
      constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
        if (new.target && DEBUG)
            return createProxyPropertyCheckker(this);
      }
    ...
    }
    

    This uses the fact that a class constructor, when called, behaves as ordinary constructor invocation. That is, if it returns an object, that object is returned by the new operator. Here new.target from ES6 is used to check that function is called as a constructor and as an ordinary function, for example, by a subclass constructor.

  • Maks Nemisj says:

    @IgorBukanov, That’s a nice addition. thanks 🙂

  • art says:
    //shorter version (taken from "You don't know js" book, Kyle Simpson)
    let pobj = new Proxy( {}, { 
      get() {
        throw "No such property/method!";
      },
      set() {
        throw "No such property/method!";
      }
    });
    
    let obj = {
      a: 1,
      foo() {
        console.log( "a:", this.a );
      }
    };
    
    // setup `obj` to fall back to `pobj`
    Object.setPrototypeOf( obj, pobj );
    
    obj.a = 3;
    obj.foo();          // a: 3
    
    obj.b = 4;          // Error: No such property/method!
    obj.bar();          // Error: No such property/method!
    
  • l2695729@mvrht.com says:

    Object.setPrototypeOf is dangerous and unpredictably slow. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf.

    Perhaps, if possible, find a way to do this with Object.create.

    But good read and good starter 🙂

  • Static One says:

    I see absolutely no problem here. If typos are a problem, I recommend you try TypeScript, PureScript, Elm or some other statically typed language that compiles to JS.

  • adrien says:

    Totally disagree. JS is a bad language for production because it is a script language. But now that you have to work with it, you cannot complain about its flows for just 1 feature. I mean you could just say, never use properties on an object because of possible misspell !!! Dynamic typing is very error prone and you’ll have to leave with this anyway, not just for getter/setter.

  • Yonathan says:

    Sure Proxy is better, but it not supported by all browsers on client side

  • Maks Nemisj says:

    @adrien, i didn’t complain about the language itself. title says “why getters/setters is a bad idea” and not “why javascript is a bad idea”, right?
    You can have your own opinion, sure, I’m okey with that, but probably you didn’t get the point. The problem I have is that mutation of an object becomes indirect and very implicit. it’s not for nothing we call it a “function”- the thing which is doing something, it’s functioning, and assignment – the fact that you assign something. Again you picked one thing from the whole article and base your comment on it. If I wanted to highlight only fact that you can misspell things, probably I wouldn’t right this arctile, but instead it would be a tweet. 🙂

  • Jonathon Hibbard says:

    I understand the concerns in the article. Javascript, as designed, is a bit “loosey goosey” when it comes to letting developers shoot themselves in the foot with things like this.

    Typos aside though, there isn’t really any negatives to using getters/setters. Getting in the habit of writing code that protects against human error is kind of a thing that a lot of engineers fall into the trap of. If that is what is needing to be protected, typically unit tests are the best way to test and prevent that from happening (and catch it when it does).

    A (over simplified and generic) unit test would be: “Given some use case with the Person Object, when I set the full name, then it should equal what I set it”.

  • sam says:

    I know this article has been out for a while but, why not use const?
    const Marc = new Person(‘Mark’, ‘Peters’);

    this makes the object non-extensible, right?

  • Chris says:

    One of the main reasons to use getters / setters is to allow code to be refactored without a breaking change / ripple effect on all downstream users of that code.

    Let’s say v1 of an object or class (rightly or wrongly) allowed users to get / set attributes directly using dot notation. Later you realize more logic is needed — some of the attributes now need setting and/or getting logic. You could change the calling convention to disallow dot notation and require function calls — yuck. Or you could change those attributes to getters / setters and nothing else changes.

  • Tim says:

    You’re right that detecting typos is a problem, but the cause isn’t setters and getters – it’s Javascript’s lack of static types. If you use Typescript it will give you an error if you misspell the property name, and it will detect typos in many other places too.

  • […] I’ve already wrote in one of my article regarding getters and setters I still thinks it’s a bad idea. It might work in languages with static type checking, where […]

  • Apena says:

    Avoiding an entire JavaScript construct for fear of mispelling property seems a bit extreme. JavaScript is flexible and versatile because it is dynamic so we work with this. There are many ways to protect against misspelling property names. Oh well thank you for your 2 cents.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.