const and let

By Mike Sharp
26 March, 2021

Github icon
Back to posts

tldr? Prefer moving pictures instead? we've got you covered, here's the video version of this blog

Today we’re going to take a look at the const and let keywords in javascript.

const and let allow us to represent and reuse values in our code just like we can with a var, but with some added benefits.

Why not use var? Why are const and let different?

var is the traditional way that we declare a variable in JavaScript, but compared to other programming languages, var has some quirks and shortcomings that can lead to unintended bugs and difficult to read code.

const and let were introduced to address the shortcomings of var, so let’s jump right into it.

Global scope vs Block scope vs Function scope

Many, if not all programming languages have the concept of scope.

Scope dictates whether a variable or constant is visible to the language interpreter at a given point in a program's execution.

Global Scope

In an application, there’s normally some form of global scope that allows you to define variables or constants that are accessible by the whole of your program.

In javascript global variables are automatically added to the Window object.

Code:

// global scope
var globalVar = "I am now part of the window object";
console.log(window.globalVar);

Output:

I am now part of the window object

It’s bad practice to use global scope because it leads to hard to read and buggy code, and because it’s added to the window object, variables and constants can easily clash with other libraries that also need to use the window object.

Code:

// simulating a library
window.someLibrary = {
  doThing: function () {
    console.log("library does a thing");
  },
};
// declaring a global
var someLibrary = {
  doThing: function () {
    console.log("I just overwrote the library...");
  },
};
// execute library method
window.someLibrary.doThing();

Output:

I just overwrote the library...

Do: Keep usage of globals to a minimum.

It’s preferable to define your variables and constants as close as possible to the code to where it’s used and for values to exist only for as long as they need to.

Do: Keep your scope localised as much as possible.

Block Scope

In many languages, the default behaviour for local variables is to use block-scope.

Block scope means that the existence of a variable or constant is confined to its closest enclosing code block, for example, a variable is declared inside a for loop, if statement or even inside an arbitrary block or function block.

// conditional block
if (true) {
  let ifThing = "if";
  // ifThing: in scope
}
// loop block
for (let i = 0; i < 5; i++) {
  let loopThing = "loop";
  // loopThing: in scope
}
// arbitrary block
{
  let blockThing = "block";
  // blockThing: in scope
}
// function block
function block() {
  let functionThing = "function";
  // functionThing: in scope
}
// ifThing: out of scope
// loopThing: out of scope
// blockThing: out of scope
// functionThing: out of scope

block-scoped variables in nested blocks:

// nested loop block
for (let i = 0; i < 5; i++) {
  let outer = "outer";
  for (let j = 0; j < 5; j++) {
    let inner = "inner";
    // outer: in scope
    // inner: in scope
  }
  // outer: in scope
  // inner: out of scope
}

Function Scope in Javascript

Javascript variables declared with the var keyword use function scope.

All vars are local to the closest function they are declared within.

Code:

function scopeFunction() {
  if (true) {
    var functionScopedValue = "thing";
  }
  // value is function-scoped, so is accessible outside 
  // of the block it is declared in
  console.log(functionScopedValue);
}
scopeFunction();

Output:

"thing"

Variable hoisting

A quirk of function scope is that all variable declarations inside a function are treated as if they are declared at the top of the function, this is called variable hoisting.

A variable that’s declared after it's used inside the same function block is valid and does not throw a javascript error because hoisting treats the variable as if it was declared at the top of the function.

Code:

function scopeFunction() {
  // function-scoped code behaves like 'hoisted'
  // is declared at top of the function. 'hoisted' can
  // be used before declared.
  for (var i = 0; i > 1; i++) {
    hoisted = "hoisted";
    console.log(hoisted);
  }
  var hoisted;
}
scopeFunction();

Output:

"hoisted"

Key point: var variables get hoisted to the top of the function they are declared in.

Enter const vs let

const and let do away with the variable hoisting and function-scoping model in favour of block scoping, which much is more conventional when you consider the majority of programming languages take this approach to variable scope.

Block-scoping without hoisting makes representing values in code less prone to logic errors by:

  • Enforcing that a variable or constant is declared before it is used and declared where it is used, making its intended use clearer
  • Keeping the scope of the variable or constant as small as possible, making the value easier to manage

Key point: const and let values are not hoisted and have block scope

Differences between const vs let

Both const and let are block-scoped as opposed to the function-scoped var, but why do we need both a const and a let keyword?

The reason is that let values can be assigned, re-assigned or not assigned at all.

const values can only be assigned once and must be assigned as soon as they are declared.

let should be used in any place where a value is expected to change, for example, a counter value that is incremented each second or as the condition in a for loop that changes on each iteration.

Code:

for (let i = 0; i < 5; i++) {
  // 'i' is in scope
  console.log(i);
}
console.log("out of block");
// should throw javascript error, i is out of scope
console.log(i);

Output:

0
1
2
3
4
out of block
Uncaught ReferenceError: i is not defined

Conversely, use const when you expect a value to remain the same through the lifecycle of the application.

Code:

// let variables can be declared without initial definition
let now;
now = new Date();
console.log(now.getTime());

// constants must be defined as soon as they are declared
const DIRECTION_UP = 1;
const DIRECTION_DOWN = 2;

// let variables can be re-assigned again and again
now = new Date();
console.log(now.getTime());

// cannot re-assign const or you'll get an error
DIRECTION_UP = 3;

Output:

1590254789382
1590254789383
Uncaught TypeError: Assignment to constant variable.

That pretty much covers all that needs to be said about let, but there are some more details to be aware of when using const, so let’s move on.

Key points:

  • let values can be reassigned, const values cannot
  • const values must be defined as soon as they are declared, let values can be declared without being defined

Control of value reassignment

Before the introduction of const, there was no built-in method in javascript to ensure a declared value doesn't get reassigned.

Javascript programmers had to rely on conventions (such as using ALL_CAPITALS for constant value names) and the clarity and code comments to distinguish const values from non-const values.

// is this a constant or was the programmer just having a bad day?
var DAYS_TILL_VACATION = 52;

// probably a constant but there's nothing stopping it being reassigned
DAYS_TILL_VACATION = 128;

As you can imagine, relying solely on conventions can result in misuse of values that are meant to be constant.

Someone could come along and re-assign a supposedly constant value partway through the code, which can lead to some pretty quirky and hard to find bugs.

The introduction of the const keyword formalizes the use of values which cannot be re-assigned and are intended to be read-only references in code.

Code:

const DAYS_TILL_VACATION = 52;

// hands off my vacation time, Derek
DAYS_TILL_VACATION = 128;

Output:

Uncaught TypeError: Assignment to constant variable.

Explicitly defining constants and having javascript assert this for us greatly reduces the ability for other programmers to misinterpret the usage of a constant.

Strongly defined constants lead to less error-prone and more intentional javascript code.

Key point: The const keyword allows us to leverage the Javacript interpreter to let us know when constants are erroneously being redefined.

Limitations of const

The const keyword is a useful addition to javascript but it is not without its caveats.

Using const ensures that a value cannot be reassigned once it is declared and that it must be assigned immediately at the time of declaration.

This is true for primitive values (numbers, strings, booleans etc), but for more complex values such as arrays and objects, the behaviour differs.

const Arrays and Objects

Arrays and objects are effectively containers for other values and const only prevents the reference to the containers from being reassigned, it does not make the contents immutable.

This means that Object properties can be added, changed and removed without breaking the rules of a value being a const.

Code:

const person = {
  name: "Mike",
  age: 28,
};
// properties on a const object can be changed
person.name = "Bob";
// new properties can be added
person.nickname = "m-dog";
// you can even delete properties
delete person.name;
// but you can't reassign the reference to a new object
person = {};

Arrays behave the same as objects, it’s perfectly valid to change the contents of a const array.

Output:

Uncaught TypeError: Assignment to constant variable.

Code:

const things = ["a", "b", "c"];
// you can push items into a const array
things.push("d");
// you can remove items
things.shift();
// you can change values by array index
things[2] = "changed";
console.log(things);
// but you can't assign a new array to the const reference
things = [];
// You can't even take the existing array 
// and spread it into a new array when its const
things = [...things, "x"];

Output:

(3) ["b", "c", "changed"]
Uncaught TypeError: Assignment to constant variable.

If you're trying to make an object or array immutable, take a look into the built-in javascript Object.freeze() function and see if that covers your use-case.

const person = {
  name: "Mike",
  age: 28,
};
// prevent properties in object from being changed
Object.freeze(person);
person.name = "John";
// frozen object, name will not be changed
console.log(person);
{name: "Mike", age: 28}

Key points:

  • const values defined using primitive types (numbers, strings or booleans) cannot be redefined
  • const Objects and Arrays act as const containers, meaning an Object or Array cannot be reassigned but the contents of the Array or Object can be modified

So this begs the question, when should let and const be used and where does this leave the var keyword?

Here are a few rule-of-thumb guidelines to help you decide which keyword to use:

  • Use const by default, unless you know that the value you're declaring is going to need to change throughout your program for your code to function correctly
  • Use let if the value you're defining needs to change as part of its behaviour
  • You don’t need to use var anymore, use a const or a let instead

Dealing with legacy code

To refactor or not to refactor?

In the real world, most legacy applications will use var exclusively in the source code.

It’s really up to you whether or not to change the old code to use const and let.

My advice is to add const and let to new pieces of functionality by default, provided that the legacy codebase doesn't disallow the use of const and let for backwards compatibility with older browsers (see the Browser Support section of this article)

Risks and considerations

Exercise caution if you’re refactoring a legacy codebase that uses vars and does a lot of function nesting or reuses variable names in various places.

There could be unintended scoping behaviour that’s not immediately clear in the application that’s become a “feature” over time.

Get to know your code better before you make sweeping changes

I recommend that you write unit tests around the code that you want to upgrade to using const and let if the application you’re refactoring is complex or is difficult to step through when debugging.

You will get a better idea of how the code behaves by writing tests and it will help you catch any changes updating the code will make.

Robust tests will also buy you an opportunity to safely clean up the code and prevent any unwanted issues, so it’s a win-win situation.

Use your best judgement and weigh it up

If it’s clear that a value is already being used like constant and the codebase has good, unambiguous names for variables then you’re probably safe to make the changes need to move your code over to using const and let, the transition over should be fairly straightforward.

Browser support

There’s good news and bad news when it comes to browser support.

The Good

The good news is that all current versions of the most popular browsers (Chrome, Firefox, Edge and Safari) support const and let, so if your target audience uses an up-to-date browser then you’re good to go and you can start using const and let instead of var. The modern browser landscape is ready for widespread use of const and let.

The Bad

The bad news is that const and let cannot be polyfilled into older browsers and running code in those older browsers will cause immediate javascript parsing errors.

The Slightly ugly (unless you already have a build process)

To mitigate this, you could add a language Transpiler such as Babel to your build process (if you have one, if you don't you will need to develop one to use a transpiler).

Babel allows you to configure the target version of javascript or target specific legacy browsers.

The transpiler then generates backwards-compatible code.

The best choice is to just use const and let natively unless you need to support older browsers.

So why not give const and let a try?

const and let are useful additions to javascript, they allow for tighter control over assignment and scope of values in your code and they are a drop-in replacement for vars.

So what are you waiting for? start using const and let today!

If you liked this blog, there's also a video version here