Mathias Bynens

ES2015 const is not about immutability

Published · tagged with JavaScript

This seems to be a very common misconception that just won’t die. I keep running into it in blog posts, Twitter discussions, and even books. Here’s my attempt at setting things straight.

const creates an immutable binding

ES2015 const does not indicate that a value is ‘constant’ or immutable. A const value can definitely change. The following is perfectly valid ES2015 code that does not throw an exception:

const foo = {};
foo.bar = 42;
console.log(foo.bar);
// → 42

The only thing that’s immutable here is the binding. const assigns a value ({}) to a variable name (foo), and guarantees that no rebinding will happen. Using an assignment operator or a unary or postfix -- or ++ operator on a const variable throws a TypeError exception:

const foo = 27;
// Any of the following uncommented lines throws an exception.
// Assignment operators:
foo = 42;
foo *= 42;
foo /= 42;
foo %= 42;
foo += 42;
foo -= 42;
foo <<= 0b101010;
foo >>= 0b101010;
foo >>>= 0b101010;
foo &= 0b101010;
foo ^= 0b101010;
foo |= 0b101010;
// Unary `--` and `++`:
--foo;
++foo;
// Postfix `--` and `++`:
foo--;
foo++;

ES2015 const has nothing to do with immutability of values.

So, how to make a value immutable?

Primitive values, i.e. numbers, strings, booleans, symbols, null, or undefined, are always immutable.

var foo = 27;
foo.bar = 42;
console.log(foo.bar);
// → `undefined`

To make an object’s values immutable, use Object.freeze(). It has been around since ES5 and is widely available nowadays.

const foo = Object.freeze({
'bar': 27
});
foo.bar = 42; // throws a TypeError exception in strict mode;
// silently fails in sloppy mode
console.log(foo.bar);
// → 27

Note that Object.freeze() is shallow: object values within a frozen object (i.e. nested objects) can still be mutated. The MDN entry on Object.freeze() provides an example deepFreeze() implementation that can be used to make object values fully immutable.

Still, Object.freeze() only works on property-value pairs. There is currently no way to make other objects such as Dates, Maps, or Sets fully immutable.

There is a proposal to add immutable data structures to a future version of ECMAScript.

const vs. let

The only difference between const and let is that const makes the contract that no rebinding will happen.

Everything I wrote here so far are facts. What follows is entirely subjective, but bear with me.

Given the above, const makes code easier to read: within its scope, a const variable always refers to the same object. With let there is no such guarantee. As a result, it makes sense to use let and const as follows in your ES2015 code:

  • use const by default
  • only use let if rebinding is needed
  • (var shouldn’t be used in ES2015)

Do you agree? Why (not)? I’m especially interested in hearing from developers who prefer let over const (i.e. even for variables that are never rebound). If you’re using let without rebinding, why are you using let in the first place? Is it because of the “const is for constants” misunderstanding, or is there another reason? Let me know in the comments!

About me

Hi there! I’m Mathias. I work on Chrome DevTools and the V8 JavaScript engine at Google. HTML, CSS, JavaScript, Unicode, performance, and security get me excited. Follow me on Twitter, Mastodon, and GitHub.

Comments

Gijs wrote on :

I think it’s a matter of opinion whether this:

let foo;
if (something) {
foo = 5;
} else {
foo = 10;
}

…is easier to read / “better” than:

if (something) {
var foo = 5;
} else {
foo = 10;
}

I certainly got used to the latter over the course of the pre-ES6 years, and don’t see the point in the extra line in the first example. But then that’s probably just me. :-)

wrote on :

Gijs: IMHO the former is much more readable, but in such a situation I tend to use the ternary operator, which avoids the issue altogether:

const foo = something ? 5 : 10;

Anyway, I’m more interested in how people use let vs. const. A lot of developers seem to use let by default for some reason, and I wonder if the whole “const is for constants!” misunderstanding is to blame.

Gijs wrote on :

Mathias: Right, I mean, clearly the example is overly simplistic — once ternaries start going over 2-3 lines the if…else version is easier to follow. The point being that IMO the var hoisting means there is still space for var over let in certain situations. Using let for globals is also problematic because of the global lexical scope (see also https://esdiscuss.org/topic/global-lexical-tier), so that’s another reason to use var in some circumstances.

wrote on :

Gijs: I strongly prefer declaring the variable outside of the if block — it’s much more readable that way, IMHO. But hey, to each their own. :)

The global let problem is interesting, but it shouldn’t impact how developers write their code. Creating global variables has always been a bad practice, except when exporting a library — and with ES6 modules and <script type=module> (and a transpiler until all browsers support it), that problem’s solved.

MaxArt wrote on :

Keep in mind that Object.freeze does not “deep-freeze” an object, i.e. object values of a frozen object won’t be frozen as well.

redoPop wrote on :

Kyle Simpson did a great job making the case against using const by default then switching to let if needed:

The mindset I have to be in to realize that a const should no longer be a const, and to then responsibly analyze the potential risk areas I am introducing by changing const back to let / var — and quite possibly needing to refactor the surrounding code more heavily to reduce those risks! — is way more than just a casual and quick, "replace const with let".

(Context, and his complete post, is here.)

So far I agree with his recommendation to use const only for variables you're planning to treat as non-reassignable.

Patrick Mueller wrote on :

From your second example:

This code does not actually throw an exception:

const foo = 27;
// Any of the following uncommented lines throws an exception.
// Assignment operators:
foo = 42;

…but this will:

'use strict'; // <<<<<<<<<<<<<<---------------------!!!!!!!!!!!!!!!!
const foo = 27;
// Any of the following uncommented lines throws an exception.
// Assignment operators:
foo = 42;

I’ve seen some folks confused by this… including myself. I’ve finally switched over to the 'use strict' life-style.

D wrote on :

To reproduce it in the Chrome Dev console:

bar = function() { 'use strict'; const foo = 1; foo = 2; }()
Uncaught TypeError: Assignment to constant variable.(…)

Moritz wrote on :

I totally agree with the const and let usage, but don’t understand why we shouldn’t use var anymore?

Why is const and var not a thing? I was under the impression that let behaves the same as var, but is block-scoped and not function-scoped.

wrote on :

Moritz: Why would you still use var (and the annoying hoisting behavior that comes with it)? There’s nothing about var that const/let can’t do better. If you need function scoping, create the const/let at the top of the function.

dino wrote on :

Moritz: The only feature var has is hoisting and it’s a feature most people didn’t want to use before. It’s confusing and all linters report it.

In the middle of const and let, a var might become something obviously used for hoisting. But it’s too early.

Moritz wrote on :

Mathias: I can’t think of a reason to use var instead of let either. I guess it just feels weird to abandon it entirely. I might also feel sorry for it, haha :D But there is also no downside in using var though (if function scoping isn’t seen as a downside)? Does code with mixed var/let look messy?

Michael J. Ryan wrote on :

Personally, I still use var a lot out of habit, I understand how it works, and how the declaration and scoping works… That said, I still think there is value in var, as a global/leaky variable, such as variables where the binding will change that are scoped at the module level, even if they are exposed via say…

var foo;
export setFoo = (newfoo) => foo = newfoo;
export getFoo = () => foo;

In this case it’s intentionally variant, leaky and loosely bound scope. I tend to mostly use let when I intentionally want a closed scope, var the rest of the time, and even then, most of the time I want to use scoping, I get that through Array.prototype.forEach/map/reduce anyway.

wrote on :

Michael: It’s still unclear why you would use var in that example — you could just use let. Block-scoping does not prevent the variable from being used in an inner scope!

Ruslan wrote on :

To me, current const state is like we had with CSS IDs 10 years ago. There were lots of “CSS classes or IDs” articles, but at the end everybody started to use IDs only for JS binding or writing tests.

The same is now happening with const — it’s just easier to use let everywhere and not think about whether the variable can be rebound or not.

I’ve been also coding in Ruby, Python and PHP, and none of those languages has such mixed variable declaration as some people want to do in ES6.

So I use const only for constants + they are always in UPPER_CASE_FORMAT.

Andrea wrote on :

I’m a victim of the “const is for constants” misunderstanding. I’ve recently started using ES6 and even more recently realized how const really works… It’s confusing at first.

Rob C wrote on :

My approach to ES6 thus far has been the same as per your recommendation:

  1. use const by default
  2. only use let if rebinding is needed.

…and so far so good in strict mode!

It appears to be the general recommendation out there:

  1. Nicholas Zakas summarises similarly here in Understanding ES6
  2. Axel Rauschmayer also here in Exploring ES6

And yes, I’ve found that I no longer use var.

wrote on :

Rob C: It seems Understanding ES6 is mistaken about what const does, though:

The current best practice for block bindings is to use const by default and only use let when you know a variable’s value needs to change. This ensures a basic level of immutability in code that can help prevent certain types of errors.

This reasoning is incorrect; const and let don’t have anything to do with whether the value of the variable changes.

Joseph N. Musser II wrote on :

I’m surprised that people find this unintuitive. It would be unhelpful and unwieldy if const obj could somehow affect whether obj.value was const.

This maps quite 1:1 to the readonly concept in C# and other languages.

Andre Behrens wrote on :

I would say if we don’t want to fool people into thinking const is for constants, then const was a poor syntactic choice.

On our team, we treat it as a constant by convention, and everyone knows what that means. Kind of like _quasiPrivate variables. For us, const means “I intend this to be constant”.

It would be nice for the language not to have features that require this much explanation.

Jonathan Cutrell wrote on :

I think probably the most compelling use case for const is going to be magic numbers.

const E = 2.7182818284;

Though it is confusing.

I think, ultimately, using const is going to be a mental trigger for future devs of that code — “hey, don’t re-assign this”. But that also requires that they know a particular thing is a const, which most people signify with caps.

Perhaps you could use const for things like window as well, so that it can only be modified, not reassigned to null or something. This may help avoid some negative scenarios.

Ville wrote on :

Seems several people in the comments don’t understand what const means even after reading the article.

Shelley Powers wrote on :

The use of var within a function anywhere is perfectly fine. Yes, it is. Until the minds behind ECMAScript decide to jump into the shark tank and deprecate var, it is a functioning part of the language. The new let and const are additions to the language, not replacements. Not unless, again, they want to come out and say, “var is going away”.

The use of var outside of a function in client-side (browser-based) JavaScript clutters the global namespace and should be discouraged… when we’re assured that applications using let will continue to work for the majority of browser users.

If we want block-level scoping than of course use let or const. If the browser allows it.

However, the use of var and let in Node has different effects. Point of fact, the use of let requires strict mode which can have other effects on the application. Some would say this is a good thing, but the effects could lead to unexpected results.

No one has been able to definitively say that the use of let or const improves program performance, or even increases the reliability of the application. So saying “never use var” or “always use const” is an opinion, not a statement based on imperative testing, or even strong anecdotal observation.

As for const in ES6, I would suggest it was not cleanly defined.

David Walsh wrote on :

Shelley: Please have a read (or re-read) of JavaScript: The Good Parts. Just because you can use some feature of the language doesn’t mean you should. As to your other points:

  • Of course TC39 is not going to break backwards compatibility by deprecating var.
  • Of course you should not use ES6 features in ES5 environments like the browser.
  • You should always be writing strict mode friendly code. The non-strict features are terrible and unnecessary. (Again see the aforementioned book.)
  • It’s not a question of performance. let and const weren’t introduced for performance reasons. Their use isn’t advocated for performance reasons.

Will wrote on :

Personally, I try to avoid all mutable state, even locally in functions. Object.freeze or ImmutableJS are invaluable for objects as this article points out, but for other primitives, const seems very useful. In ES5 I gave up ever needing the else keyword. In ES6 I’ll be able to give up var and let will be a complete non-starter.

Ondrej Hanslik wrote on :

Gijs: Actually, anytime you think you need to use let just for a complex value initialization, you are doing it wrong. You don’t have to use a ternary operator, you can simply create a function to return you the value. JavaScript is all about functions and closures. let should be used sparingly and var never.

var is staying only and only because of backward compatibility. In most programs almost all the local variables should be const.

David Wilhelm wrote on :

First, I do understand the behaviour of const. But it seems to me that the ‘best practice’ of always using const is problematic. Surely, the value of the const keyword is also to communicate the developer’s intention that the value should not be changed. If we always write const instead of let, how are other developers meant to know whether they should write code that changes/reassigns to the variable, by changing it to let? If we use let by default, and only use const for values which are never meant to change, then it is clear.

To me it seems weird that an object literal assigned to const isn’t deep-frozen by default, I expect many will be tripped up by that. Great, we just added to the list of JS “gotchas”. This seems like a good reason to avoid using const for objects, just to avoid the confusion. After all, WTF is the point of a const object that can be mutated anyways?

Ville wrote on :

const communicates exactly the developer intent that he won’t be rebinding the variable within that scope. It doesn’t communicate that the developer will never mutate the referenced variable, because that’s how it has been specified. Any other intent would be a misunderstanding of the language.

let communicates that you will be rebinding the variable later in the scope.

var communicates that you are not yet familiar with ES6 ;)

David Wilhelm wrote on :

Yes, but the thing is that code changes over time… At first there may be no re-assignment to the const. But later, another developer makes changes to the code to introduce a reassignment. So he’s like “I need to change the value of this const, so I’ll just change it to a let”. But this may be wrong, and introduce bugs. So this is why I think, better to just use let, to indicate a variable can be changed/reassigned (the same thing for plain values), and use const to indicate “this should never change” so the other developers don’t just change it to let when they want to change it.

John wrote on :

I don’t care about rebindability!

I have many use cases for having immutable variables, and not many use cases for having an object that I cannot rebind but can change his value.

So for all primitives const = immutable, so I use that to express immutability (with caps as convention) and for objects, I just avoid using const, since I don’t care about rebindability, and have no easy option for immutability, const will make it confusing.

Oh, and 'const'.length == 5, while let is significantly shorter :-)

redoPop wrote on :

I agree, David Wilhelm. The adage that “if everything is important, nothing is” applies here: using const by default for anything that isn’t rebound diminishes its usefulness as a flag for things that shouldn’t be rebound.

Something Mathias said in his post bears thinking about more deeply:

const makes code easier to read: within its scope, a const variable always refers to the same object. With let there is no such guarantee.

Using const by default makes it harder to differentiate between circumstances where this guarantee is actually important vs. ones where the developer has used const just because that’s their go-to declaration keyword.

Ville wrote on :

const is not the important keyword if it’s the default (almost all variables in normal JS code will be const). let is the one that means “pay special attention to this — I will rebind it”.

Neither keyword creates bugs as the transpiler will error out on misuse.

David Walsh wrote on :

I really like the idea of const by default. It’s a practice I’ve adopted in my ES6 code.

const just means the variable (sic) can only be assigned once. Once you realise this, using const by default isn’t such a big hang up. In fact, I find you seldom need to use let.

About the only uses I can think of for let are (a) loop variables or (b) conditional assignments too big for a ternary. But (a) can generally be replaced with array methods like forEach or map; whilst (b) is often better refactored into another smaller function (with multiple returns instead of multiple assignments).

There’s no use pretending const is for immutability and let is for mutability. For one thing, once you’re aware of the benefits of immutability you should be using it all the time (if possible) anyway. And if you want to enforce it, use Object.freeze or Immutable.js.

James Gardner wrote on :

I’ll echo any view that we shouldn’t simply blanket-replace var with const or let. The gripe I have with ES6 is that most of the new stuff doesn’t solve core problems in the language itself and that what’s driving this effort is purely political. An effort to adopt more familiar constructs like class and const for the sake of accessibility. I find const to be another example of this as the keyword is familiar but misleading. Why not fix var or introduce a fully immutable type instead? ES6 seems like a wasted opportunity to me. Meh.

wrote on :

James:

I’ll echo any view that we shouldn’t simply blanket-replace var with const or let.

No one said you should (at least not on this page). When rewriting existing code as ES6, you should carefully consider each variable and decide to use either const or let. It’s not a simple search/replace thing. But for new code, there’s no reason to use var anymore.

I find const to be another example of this as the keyword is familiar but misleading.

How so? It’s exactly the same behavior as in other programming languages. Only C++ is different. The name const is only confusing if you (incorrectly) think of the value rather than the binding as the constant thing. Don’t do that.

Why not fix var […]?

That’s what TC39 did when they introduced let and const. The semantics of var itself cannot be changed as that would break backwards compatibility.

ThomasR wrote on :

Most of the time reassignment really does't do any harm. Personally, I prefer to use let by default. I only use const to make absolutely clear that the variable must not be reassigned.

For exaple, I'd write

const foo = require('foo');
const someId = 'abc-42';

but

let x = new Qux();
let bar = {x: 5};

Why? In my mind, the "default access permission" in JS has always been read/write.

Consider the analogy of adding a property to an object:

o.x = 1; // x is writable (~ let)

If you want to prevent reassignment of o.x, you have to explicitly do so:

Object.defineProperty(o, 'x', {value: 1, writable: false}); // x is read-only (~ const)

People hardly ever do that, so why would I use const by default.

Other than that, var is dead.

gotofritz wrote on :

The problem I have with const is:

const a = something ? 1 : 0;
const b = something ? 10 : 0;
const c = something ? 100 : 0;

So we are repeating the something test many times (it could be an expensive function)? The obvious thing to do would be:

if (something) {
const a = 1;
const b = 10;
const c = 100;
} else {
//
}

…but that doesn’t work because of block scoping. The only things that work is to use either var or let:

let a, b, c; 
if (something) {
a = 1;
b = 10;
c = 100;
} else {
///
}

In other words, I start off with const and then as my code evolves have to move to let because of other constraints. I find this distracting, so I’d rather go with let by default and use const only for magic numbers.

Rodney Reid wrote on :

Shelley:

No one has been able to definitively say that the use of let or const improves program performance, or even increases the reliability of the application.

For a while, let was actually slower than var by a couple percentage points. I can’t prove in current Chrome Canary 50 or Firefox Nightly 47 that this is still the case though.

Glenjamin wrote on :

Is multiple assignment a problem people have been having that causes bugs?

Especially when using a linter and avoiding for loops, the problems of var don’t really come up.

For this reason I’m not leaping to adopt const and let, in the cases where they make intent clearer, I suspect the function being too long is why intent is unclear — and prefer to address that instead.

Nijiko Yonskai wrote on :

gotofritz: This problem is solved by better variable naming, and is in fact not an actual issue.


In regards to the other items, looking in other languages that are more powerful or knowing languages that are more powerful allow you to have a better idea of the benefits of defaulting to const you realize that the variable will be set in stone, let throws a flag that it can change.

That is your ideal state.

Question to propose: Should const run Object.freeze to ensure the appropriate functionality to its children properties?

Second question: Should Object.freeze also do a deep freeze functionality?

Third question: Should those be false, should const be changed, as it doesn’t do what it implies?

Fagner Brack wrote on :

Come on. I don’t know why this post was tagged as “opinion” in JavaScript Weekly. Start variables with reduced visibility and referencing lock is a proven pattern that helps to prevent unintentional mistakes by making the intent clear, which is also understood by the programming language. It has nothing to do with readability arguments.

Mike Schwab wrote on :

I suggest avoiding foo and bar, etc.

By using variables that have no resonance, you prevent the brain from understanding the lesson. In particular, I completely missed your point about mutability the first time I read the article because you used foo for two completely different things.

I feel this way about every code example, but particularly for something so abstract I hope you can understand why I find this tradition to be a bane against comprehension. Thanks!

steviesama wrote on :

Gijs: I think not using var in the one example, and/or not leading with the declaration of the variable before the if block scope wouldn’t be practical for any code that ended up with 'use strict' as it would throw errors.

Sander wrote on :

Gijs: Correct me if I’m wrong, but in your second example you make the second foo global, by not writing var in front of it. So your first example is better.

wrote on :

Sander: That’s incorrect. Because var hoists, the code snippet Gijs provided, i.e.

if (something) {
var foo = 5;
} else {
foo = 10;
}

…is equivalent to:

var foo;
if (something) {
foo = 5;
} else {
foo = 10;
}

let and const avoid hoisting-related confusion.

nnnnnn wrote on :

A const value can definitely change

No it can’t. When you say const x = {}, the value being assigned is a reference to the object, not the object itself, and that reference cannot be changed.

But yes, you are right that it isn’t immediately obvious to everyone that the object itself can be modified, including being frozen later with Object.freeze(x).

art wrote on :

A good difference between let and var (at least) is explained on the chapter 2 of ES6 & Beyond by Kyle Simpson:

The let i in the for header declares an i not just for the for loop itself, but it redeclares a new i for each iteration of the loop. That means that closures created inside the loop iteration close over those per-iteration variables the way you’d expect.

Try the two loops below — the one with the let is shorter.

for (var i = -1; ++i < 10;) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, 1000 * j);
}(i));
}

// vs.

for (let i = -1; ++i < 10;) {
setTimeout(() => console.log(i), 1000 * i);
}

Serge Krul wrote on :

Mathias: Well technically is is immutable value-wise since when you change a property on the referenced object you don’t change the value of the variable, which is the reference to the object.

Ivan Kleshnin wrote on :

To me, current const state is like we had with CSS IDs 10 years ago. There were lots of “CSS classes or IDs” articles, but at the end everybody started to use IDs only for JS binding or writing tests. The same is now happening with const — it’s just easier to use let everywhere and not think about whether the variable can be rebound or not.

Exactly. const is like an CSS id — praised but worthless. I described my preference for let here: https://github.com/Paqmind/styleguides#let-vs-const.

As const doest not reduce the number of bugs (never had bugs because of variable redeclarations, had tons of bugs because of mutability in the past), does not improve readability and is a PITA to write – why bother?

Oenonono wrote on :

I genuinely don’t see any reason to drop var or wholesale replace it with let and const. The only epidemic problem with var is forgetting to use it, which one could equally do with const or let. Lexical scoping doesn’t improve readability or reliability outside of a few cases like for loops; anything that is confusing about redefining a variable is as or potentially more confusing with let, though I imagine a specific developer’s background would have a big impact on that perception.

As far as const is concerned, what you said here has (unsurprisingly) been widely adopted. But from a semantic perspective, I find it misleading. const is short for “constant”, is it not? If it’s not a constant value and it’s not a “constant” in the sense it’s usually used in module design, then I’m not gaining anything from it. All I’ve done is introduce a new and essentially useless and perhaps even confusing construct for a coworker to parse. I still don’t see the point.

Intent is readability. When you use something like const you’re saying something about your intent, but if what a reader would expect that message to be differs from the reality, you done fucked up. Intent failed.

Chris wrote on :

Ivan: const helps prevent accidental type coercion from happening on data structures or functions. Consider this code:

let position = { x: 2, y: 3 };

Then, somebody accidentally does this:

position += 1;

Now position is this string:

'[object Object]1'

You may not even see a runtime error depending on what happens. After the type coercion, position.x does not cause an error, it returns undefined. So your code is now poisoned and you have no guarantee of how bad it might be or how much information you are going to get from the runtime about it.

However, if position was declared as const, you would get a type error telling you the line where the mistake is, and the rest of the code would still work since the object cannot be coerced into an unexpected type.

const is great. It helps me sleep at night.

Leave a comment

Comment on “ES2015 const is not about immutability”

Your input will be parsed as Markdown.