With ES2015, we get the let keyword. This adds block scoping to JavaScript. It’s a welcome addition to the language, but you don’t always need to use it. Sometimes it’s perfectly fine to stick with our old friend var.

Scoping in JavaScript

Function scope

Prior to ES2015, JavaScript only had function scope. Block scope, while common in many other languages, did not exist in JavaScript. Consider the code:

1
2
3
4
5
function foo(bar) {
  if (bar) {
    var baz = 'Bar is truthy!';
  }
}

Newcomers to JavaScript might think that baz is only available inside the if statement block. With var, this isn’t the case. The declaration of baz is “hoisted” to the top of its scope, which is the function foo.

If we add a couple of console.log to statements foo, before and after the if statement, we can see this behavior:

1
2
3
4
5
6
7
function foo(bar) {
  console.log(baz); // undefined
  if (bar) {
    var baz = 'Bar is truthy!';
  }
  console.log(baz); // 'Bar is truthy!' (if bar is truthy)
}

Because of hoisting, we don’t get an error on line 2. The declaration of baz is hoisted, but the initialization is not.

Block scope

The addition of the let keyword gives us block scoping. Here’s the updated example, using let:

1
2
3
4
5
6
function foo(bar) {
  console.log(baz); // ReferenceError: baz is not defined
  if (bar) {
    let baz = 'Bar is truthy!';
  }
}

Because we used let, baz is not visible outside of the if statement. Trying to access baz results in a ReferenceError.

Temporal dead zone

Unlike var, let declarations do not get hoisted to the top of their scope. JavaScript has a fancy term for this: the so-called “temporal dead zone”. All this really means is that you will get an error if you try to reference a variable declared with let before its declaration in the scope. Continuing our contrived example function:

1
2
3
4
5
6
function foo(bar) {
  if (bar) {
    console.log(baz); // ReferenceError: baz is not defined
    let baz = 'Bar is truthy!';
  }
}

The reason I bring up hoisting and the temporal dead zone is to demonstrate that other than unit of scope, let and var have different semantics.

When to use let

There’s nothing wrong with let. Block scoping is something that has been sorely lacking in JavaScript for a long time. If you have a variable inside of a block (an if statement or for loop, for example), you probably want to use let.

Should we still use var?

In my humble opinion, var is still perfectly acceptable to use if you don’t need block scope. If your variable is used throughout the entire function, you probably want to use var. Why? let would work just as well here. While this is true, the different semantics discussed above may cause unintended consequences.

I’ve seen a lot of people advocating for a wholesale replacement of var with let across an entire codebase, or have standards forbidding the use of var in new code. I don’t think either of these are a very good idea. You should use the right tool for the job. If you don’t need block scope, use var. If you do, use let. Be aware of hoisting and the temporal dead zone if you start changing your vars to lets.

Don’t forget about const!

ES2015 also gave us the const keyword. A variable declared with const cannot be changed after it is initialized. Unexpectedly changing variables in a program can introduce subtle bugs. As my personal style, I prefer const over let or var whenever possible because it protects you against accidentally changing a variable’s value. If only there was a way to declare function arguments as consts…

Summary

In summary, here is my humble advice:

  • Use const wherever possible. It’s easy to change a const to a var or let later if needed.

  • If the variable needs to change, and you need block scope, use let.

  • If you only need function scope, it’s OK to still use var.