Why You Shouldn't Reassign Variables

JavaScript Best Practices

The Short Answer

By avoiding variable reassignment, you improve clarity, maintainability, and minimize side-effects.

But Why?

Let's start by comparing some of the different kinds of variables in JavaScript.

  • let  - This will define a block-scoped variable which may be reassigned.

  • const  - A constant, by definition, is not technically a variable at all. As such, it cannot be reassigned. This is also block-scoped just like let .

  • var  - This is all we used to have available, before ES6. Whether we wanted these definitions to be treated as constants or not, it was always possible to reassign them. Constants were on more of an "honor system" when you named something in all caps. var  is slightly different from let  because it's not block-scoped, it's only function -scoped. var  declarations are also hoisted to the top of the script, so they will always exist as undefined until they are assigned later. const  and let , however, will not exist until they are assigned.

It's okay if you didn't follow all that, the important take-away here is that var  and let  can be reassigned, while const  cannot. Since it's typically best to avoid reassignment, it's great to prefer const  over let  and var . To quote the Airbnb styleguide :

This ensures that you can’t reassign your references, which can lead to bugs and difficult to comprehend code.

If you've ever worked on a large legacy function that had a dozen nested layers of if  statements, you'll sympathize with this statement. I once spent days fixing every edge case for a particular variable that was changed in a hundred different scenarios. The indiscriminate reassignment in that case gave rise to bugs and code that was nearly impossible to follow.

But there's another reason to prefer const  too: consistent typing.

var list;
// ... some code
if (condition) list = ['green eggs', 'ham'];
// ... some code
list.forEach(item => console.log(item));

RESULT: Uncaught TypeError: Cannot read property 'forEach' of undefined

Even though we've only declared the variable without assigning anything, this is still considered a case of reassignment. That's because var list;  is essentially shorthand for var list = undefined; . By declaring it and then assigning it later, we change its type as well as its value. That's what causes the error we see above. Assigning things just once allows us to neatly collect all the information in one place, and enables us to keep a consistent type.

const list = condition ? ['green eggs', 'ham'] : [];
// ... some code
list.forEach(item => console.log(item));

Now we know, if the condition is false, we will still be guaranteed that list is an array. If it's empty, nothing gets logged to the console, but no errors occur that break the script.

Along this same vein of thought, I've very often found code that looks like this in legacy environments.

var color;
if (type === 'background') {
  color = 'white';
} else {
  color = 'black';
}

If you've ever read our beloved Uncle Bob's Clean Code , you might recognize this snippet as a suggestion in his book to avoid ternaries. I'm as big a fan of Bob as anyone else, but this is something I would contest. I think it's important to be conscious of how you're using ternaries, to be careful about making them as concise as possible and never nested if it can be helped. That said, I think ternaries are your friend when it comes to assigning values.

Take this same snippet, for example, and try to apply it to a constant.

const color = 'none';
if (type === 'background') {
  color = 'white';
} else {
  color = 'black';
}

RESULT: Uncaught TypeError: Assignment to constant variable.

It won't work, because as mentioned, constants cannot be reassigned. Now let's use a ternary to write it instead, being careful to keep the condition limited to a single variable (or function) so that it stays legible.

const isBackground = type === 'background';
const color = isBackground ? 'white' : 'black';

Now that looks way easier to digest, right? But there's an added benefit: you no longer have any reassignment, so you can enforce the constant.

Now, what if we don't have the luxury of a simple ternary statement? Maybe there's just too many conditions to narrow it down to that level. In that case, you can write a function to return the value that you want, though even that can sometimes be cumbersome to read and write. A common pattern I've found myself adopting is an object literal using keys in lieu of cases from a switch statement.

const colorMappings = {
  background: 'white',
  text: 'black',
  link: 'blue',
  price: 'green'
};
const color = colorMappings[type];

Now everything is still easy enough to read and understand, plus you still avoid reassignment issues.

Is it Ever Okay to Reassign?

Of course, sometimes it's even better to use let , but those cases are pretty few and far between. For example, if you're trying to write super efficient code through the lens of Big-O algorithms, you'll find it nearly impossible to write it without let . As a good rule of thumb, if you find yourself with no other choice than to use let , I would suggest to keep its scope as small as possible. Containing it to a few lines will reduce the amount of effort in locating all the mutations.

Whatever your thoughts on const  vs let  may be, I think we can all agree on at least one thing. If a value does not mutate, we should not use let at all, because its usage signals to other developers that this value is expected to change. I've seen them scattered about in code before, and it always frustrated me when I would spend fifteen minutes searching for a mutation that never existed. At the end of the day, team work is most important, after all.

Going beyond simple variables, we also have the case of object properties. Typically, when we mutate object properties, it's considered some form of "state," but perhaps that's another topic for another time. For now, just understand that state management is not to eliminate side-effects, but rather contain them so they're easy to track and debug. Reassignment is inevitable, but if you avoid directly mutating property values in favor of setters or actions, you should find it pretty easy to follow.

Well I won't ramble on forever, but I hope you found this useful! My goal is to post things that I would have wanted to see when I was just starting my career. Whether you agree with my sentiments or not, I hope you at least found this enlightening in some way!