Let It Be - How to declare JavaScript variables

Wow! This takes me back! Please check the date this post was authored, as it may no longer be relevant in a modern context.

ES2015 added a variety of riches to JavaScript. Among them are two new ways to declare variables, let and const. These tools were introduced to address faults of var, provide us an opt-in path to new functionality, and bring richers semantics to JavaScript variable declarations.

Let’s talk about const, let, and how we declare variables in JavaScript.

Why let?

Before ES2015 there was a single way to declare variables in JavaScript. This one tool was var. Since JavaScript is additive (changing the meaning of var would break some number of websites) developers can still use var.

There are some problems with the way var works that needed to be fixed.

Variables declared with var are scoped to a function, not to a block.

For example this is valid use of var:

// In strict mode
function hello(place) {
  if (place === 'world') {
    var string = 'SO CREATIVE OF YOU';
  }
  return string || `hello ${place}`;
}

hello('friend'); // => "hello friend"
hello('world');  // => "SO CREATIVE OF YOU"

In this example

  • string is “hoisted”. It is declared at the function’s scope (with a default value of undefined).
  • string is scoped to the function. When assigned a value, that value remains for the whole function.

By comparison let variables are only scoped to their block. For example:

// In strict mode
function hello(place) {
  if (place === 'world') {
    let string = 'SO CREATIVE OF YOU';
  }
  return string || `hello ${place}`;
}

hello('friend'); // => throws ReferenceError: string is not defined
hello('world');  // => throws ReferenceError: string is not defined

In the case of “friend”, string is not declared. Its use after return is an invalid reference. In the case of “world”, string is declared and provided a value, however that declaration and value pass out of scope after the following }.

var is commonly mistaken for having block scope. For example, with for:

for (var i = 0; i < 10; i++) {
  // Some logic
}
// i has leaked out of the for loop's { } block.
console.log(i); // => logs "10"

By producing code with shorter variable lifetimes by default, let helps developers avoid common foot-guns. When long-lived variables are used, they are intentional and easy to identify.

Second, var declared variables can de re-declared. For example:

// In strict mode
function hello(place) {
  var string = `hello ${place}`;
  // Some logic...
  // Declare string, again
  var string = 'SO CREATIVE OF YOU';
  return string;
}

hello('friend'); // => "SO CREATIVE OF YOU"

If you found the above code in a program, you would presume the author did not intend to over-write the initial declaration of string. Code like this is almost certainly in error.

let will throw an exception when a variable is re-declared in the same scope. For example:

// In strict mode
function hello(place) {
  let string = `hello ${place}`;
  // Some logic...
  // Declare string, again
  let string = 'SO CREATIVE OF YOU';
  return string;
}

// Throws on compilation with SyntaxError: Identifier 'string' has already been declared

This improvement eliminates the silent failure of variable naming collisions. Instead the developer is notified before the script executes that there is an error.

Finally, let can be used with a new feature of the language: Destructuring. For example:

node.addEventListener('click', function(event) {
  let { target } = event;
  jQuery(target).addClass('clicked');
}

Destructuring is one of my favorite language features arriving in JavaScript, and you can only get it with let, const and with var. Learn more about destructuring in this great blog post at 2ality.com.

Why const?

Constants are a feature of many languages. A constant variable is assigned a value once at declaration and rejects any attempts to change the value. An example of constant usage in Ruby:

A_CONST = 'foo';
A_CONST = 'baz'; # warning: already initialized constant FOO

This kind of variable is useful in JavaScript as well. To create constants in JavaScript, developers use the const keyword:

// parity.js
export const EVEN = Symbol('even');
export const ODD = Symbol('odd');

function parity(integer) {
  return integer % 2 ? ODD : EVEN;
}

export default parity;
import { ODD, EVEN }, parity from 'parity';

let number = 23;
switch (parity(number)) {
  case ODD:
    console.log(`How odd number ${number}!`);
    break;
  case EVEN:
    console.log(`Even-Steven ${number}`);
    break;
  default:
    throw new Error("That ain't good");
}

Two goals are met using const and CAPS in the above code:

  • The variables cannot be given a new value. Setting a const would raise an exception.
  • The code, by using const and CAPS, hints to the reader that changing these values would break the program.

Note that const variables are protected from re-assignment, but are still mutable. This is a common point of confusion. For example:

const NAME = 'Bob Marley';

// Attempting to re-assign a const raises an exception
NAME = 'Rick Astley'; // throws TypeError: Assignment to constant variable.
const FULL_NAME = {
  first: 'Bob',
  last: 'Marley'
};

// Re-assigning any value, including an object raises an exception
FULL_NAME = {
  first: 'Rick',
  last: 'Astley'
}; // throws TypeError: Assignment to constant variable.

// Modifiying properties on an object is just fine though
FULL_NAME.first = 'Rick';
FULL_NAME.first; // => 'Rick';

FULL_NAME cannot have a new value assigned, but properties on the object that is its value can be assigned new values. Changing properties of an object or members of an array assigned to a const is permitted.

A const variable cannot be re-assigned, but the value of a const is still mutable.

The semantic justification for const is driven home by this limitation. It can sometimes makes sense to use const even if the value object remains mutable. The lexical meaning of const is more important its behavior. An example:

const SAMPLE_RATES = {
  LOW: 'Nick',
  HIGH: 'Nancy'
}

This code does not protect SAMPLE_RATES.LOW from being assigned a new value, however it would certainly be bad form to do so. const has semantic value that expresses the author did not intend these values to change. It suggests to future travelers that they should tread lightly.

Liberal let

Both let and const are great additions to the language. However they’ve also introduced some new complexity.

When declaring a variable, should you use var, let, or const?

If you can, do not use var. The problems that exist with it are real, and you will write better code without it.

So what are the guidelines for choosing between let and const?

For most variables, use let. For constants, use const. Constants should be declared at the top of modules and only in module scope.

Some things that should be declared with const are:

  • Configuration values that are defined once, such as const TEMPLATE = 'Hi %s!'
  • Most exported variables. For example export const ODD = Symbol('odd')
  • Bespoke classes, like const MyInputComponent = Ember.Component.extend()
  • Any values imported to a module are already constants, and will raise an exception if you attempt to re-assign them

Some things that should be declared with let:

  • Most variables, especially any that are not at the module level. If it is inside a function or inside a block, use let
  • Any values that are function arguments already behave like let declarations, though you may not want to re-assign them for performance reasons (browser JITs don’t seem to like it).

This example uses let and const in an ideal manner:

// Constants and imports are grouped together, and use CAPS
import PLACEHOLDER from 'placeholder';
const TEMPLATE = `Howdy ${PLACEHOLDER}!`;
const SPECIAL_GUEST = 'Warren';

// Variables in `welcome` use `let`, regardless of if they change
function welcome(name) {
  let message = TEMPLATE.replace(PLACEHOLDER, name);
  if (name === SPECIAL_GUEST) {
    message += ' You old so and so';
  }
  let wordCount = message.split(' ').length;
  return {message, wordCount};
}

welcome('Rich'); // => { message: 'Howdy Rich!', wordCount: 2 }

Constantly const

There is another school of thought on when to use let and const I’ll need to address. This strategy suggests developers use const as much as possible. Any variable that is not re-assigned should be declared with const. For example:

// Re-assigned variables use `let`, if they are assigned once use `const`
function welcome(name) {
  let message = TEMPLATE.replace(PLACEHOLDER, name);
  if (name === SPECIAL_GUEST) {
    message += ' You old so and so';
  }
  const wordCount = message.split(' ').length;
  return {message, wordCount};
}

I think this usage is poor practice. It adds an extra distraction to the process of programming, and results in code difficult to understand and change.

First, aggressive use of const devalues the operator. Take this example:

import PLACEHOLDER from 'placeholder';
const TEMPLATE = `Howdy ${PLACEHOLDER}!`;

function welcome(name) {
  const message = TEMPLATE.replace(PLACEHOLDER, name);
  const wordCount = message.split(' ').length;
  return {message, wordCount};
}

Why is message a const? “Constantly const” say it should be so because it doesn’t change. So what if we add a feature to this code:

import PLACEHOLDER from 'placeholder';
const TEMPLATE = `Howdy ${PLACEHOLDER}!`;
const SPECIAL_GUEST = 'Warren';

function welcome(name) {
  const message = TEMPLATE.replace(PLACEHOLDER, name);
  if (name === SPECIAL_GUEST) {
    // Wait! You can't change a `const` like this!
    message += ' You old so and so';
  }
  const wordCount = message.split(' ').length;
  return {message, wordCount};
}

The suggestion is that now, when adding a re-assignment, the developer should go back and change the message declaration to let. What was the value of making it a constant then? In comparison, TEMPLATE and SPECIAL_GUEST are true constants. A developer knows that they should think twice before changing their declaration.

We showed above with SAMPLE_RATES.LOW that the semantics of const are important. Even though LOW can be changed, const hints to the reader of the author’s intent. The semantics of const are more important than the behavior.

Constantly const advocates suggest there is “expressiveness” in having all variables with stable values use const. There are two holes in this logic:

  • By const being the default declaration, let rises as the more visible style of declaration. The idea is that let flags where something funny is happening. However, function arguments like function(a,b,c) { are also allowed re-assignment, so it is a false sense of security to suggest no let means no funny business is happening.
  • What is “expressed” by const itself when used this way? Since you are intended to refactor the declaration to let if the situation requires it, it can only express “this variable wasn’t being re-assigned when I wrote this code, but feel free to change that”. This is basically meaningless. In comparison, the expression of const in liberal let it akin to “this should not be re-assigned, proceed with caution!” which is far more useful.

Some adherents of this style even suggest developers should use const but change to let only after their build tooling complains! To opt into such a slow process for little or no benefit is just short of madness.

Second, choosing const first really means choosing to think about every declaration. Will the next line of code change this assignment? How will I use this variable? Choosing let first eliminates this intellectual load, helping developers focus on more important problems.

Given that you write a program like the following (declarations as ____):

function sortClowns(clowns) {
  ____ length = clowns.length;
  for (____ i=0; i<length; i++) {
  • When declaring length, a developer is already thinking about if the algorithm involves decrementing that number to zero or not. You might not think you considered it, but your brain did.
  • When declaring i, it is unreasonable to suggest any developer would declare it with const first, just to type i++ a few characters later then go back to make the declaration let. This kind of second-guessing happens all the time when developers choose const-first.

A common third argument in favor of constantly const is the incorrect perception that it may improve performance. The arguments goes: Because browers know a variable will not change, they can better optimize… something. There is no known performance difference between let and const. Despite any intuition we, as web developers, may have for what a JIT should and should not optimize, there is no justification for const on performance grounds. The performance rational is mostly based on confusion with the benefits of immutable data structures.

Because it is more distracting to write, and decreases the explicit semantics of const, I do not suggest you write code const-first. Stick with liberal let, and focus on solving actual programming problems.

Automate, Automate, Automate

I feel pretty strongly that liberal let is a better strategy than constantly const, but there is one more suggestion I want to drive home.

Whatever style of declaration you adopt, automate the enforcement of idiomatic usage in your codebase. Nothing wastes developer time more than going back to a PR because they haven’t memorized the style guide.

There are a number of tools that can help with this:

Use automated style-guide enforcement to ensure, no matter how you declare variables, that your codebase is consistent.