For and against `let`

In this post I'm going to examine the case for (and perhaps against?) one of the new features coming in JavaScript ES6: the let keyword. let enables a new form of scoping not previously accessible generally to JS developers: block scoping.

You may have heard the term "hoisting" to describe how JS var declarations are treated within scopes. It's not exactly a technical description for how it works, but more a metaphor. But for our purposes here, it's good enough to illustrate. That above snippet is essentially treated as if it had been written like:

As you can see, the foo() function declaration was moved (aka "hoisted", aka lifted) to the top of its scope, and similarly the bar, baz, and bam variables were hoisted to the top of their scope.

Because JS variables have always behaved in this hoisting manner, many developers choose to automatically put their var declarations at the top of each (function) scope, so as to match code style to its behavior. And it's a perfectly valid way of going about things.

But have you ever seen code which does that, but also will do things like this in the same code:

The var tmp inside the if block sorta violates the ostensible "move all declarations to the top" coding style. Same of the var i in the for loop in the earlier snippet.

In both cases, the variables will "hoist" anyway, so why do developers still put those variable declarations deeper into the scope instead of at the top, especially if all the other declarations have already been manually moved?

Block Scoping

The most salient reason is because developers (often instinctively) want some variables to act as if they belong to a smaller, more limited section of the scope. In specific terms, there are cases where we want to scope a variable declaration to the block that it's solely associated with.

In the for (var i=..) .. case, it's almost universal that the developer intends for the i to only be used for the purposes of that loop, and not outside of it. In other words, the developer is putting the var i declaration in the for loop to stylistically signal to everyone else -- and their future self! -- that the i belongs to the for loop only. Same with the var tmp inside that if statement. tmp is a temporary variable, and only exists for the purposes of that if block.

Stylistically, we're saying: "don't use the variable anywhere else but right here".

Principle of Least Privilege

There's a software engineering called "principle of least privilege (or exposure)", which suggests that proper software design hides details unless and until it's necessary to expose them. We often do exactly this in module design, by hiding private variables and functions inside a closure, and exposing only a smaller subset of functions/properties as the public API.

Block scoping is an extension of this same mindset. What we're suggesting is, proper software puts variables as close as possible, and as far down in scoping/blocking as possible, to where it's going to be used.

You already instinctively know this exact principle. You already know that we don't make all variables global, even though in some cases that would be easier. Why? Because it's bad design. It's a design that will lead to (unintentional) collisions, which will lead to bugs.

So, you stick your variables inside the function they are used by. And when you nest functions inside of other functions, you nest variables inside those inner functions, as necessary and appropriate. And so on.

Block scoping simply says, I want to be able to treat a { .. } block as a scope, without having to make a new function to encapsulate that scope.

You're following the principle by saying, "If I'm going to only use i for this for loop, I'll put it right in the for loop definition."

JS Missing Block Scoping

Unfortunately, JS has not historically had any practical way to enforce this scoping style, so it's been up to best behavior to respect the style being signaled. Of course, the lack of enforcement means these things get violated, and sometimes it's OK while other times it leads to bugs.

Other languages (e.g., Java, C++) have true block scoping, where you can declare a variable to belong to a specific block instead of to the surrounding scope/function. Developers from those languages know well the benefits of using block scoping for some of their declarations.

They often feel JS has been lacking in expressive capability by missing a way to make an inline scope within a { .. } block instead of the heavier-weight inline function definition (aka IIFE -- Immediately Invoked Function Expression).

And they're totally right. JavaScript has been missing block scoping. Specifically, we've been missing a syntactic way to enforce what we already are comfortable expressing stylistically.

Not Everything

Even in languages that have block scoping, not every variable declaration ends up block scoped.

Take any well-written code base from such a language, and you are certainly going to find some variable declarations that exist at the function level, and others which exist at smaller block levels. Why?

Because that's a natural requirement of how we write software. Sometimes we have a variable we're going to use everywhere in the function, and sometimes we have a variable that we're going to use in just a very limited place. It's certainly not an all-or-nothing proposition.

Proof? Function parameters. Those are variables that exist for the entire function's scope. To my knowledge, no one seriously advances the idea that functions shouldn't have explicit named-parameters because they wouldn't be "block scoped", because most reasonable developers know what I'm asserting here:

Block scoping and function scoping are both valid and both useful, not just one or the other. This kind of code would be quite silly:

You almost certainly wouldn't write code like that, just to have a "block scoping only" mentality about code structure, anymore than you'd have x and y be global variables in a "global scoping only" mentality.

No, you'd just name the x and y parameters, and use them wherever in the function you need.

The same would be true of any other variable declarations you might create which you intend and need to use across the entire function. You'd probably just put a var at the top of the function and move on.

Introducing let

Now that you understand why block scoping is important, and importantly swallowed the sanity check that it amends function/global scoping rather than replacing it, we can be excited that ES6 is finally introducing a direct mechanism for block scoping, using the let keyword.

In its most basic form, let is a sibling to var. But declarations made with let are scoped to the blocks in which they occur, rather than being "hoisted" to the enclosing function's scope as vars do:

Yay! let declarations not only express but also enforce block scoping!

Basically, any place a block occurs (like a { .. } pair), a let can create a block scoped declaration inside it. So wherever you need to create limited-scope declarations, use let.

Note: Yeah, let doesn't exist pre-ES6. But quite a few ES6-to-ES5 transpilers exist -- for example: traceur, 6to5, and Continuum -- which will take your ES6 let usage (along with most of the rest of ES6!) and convert it to ES5 (and sometimes ES3) code that will run in all relevant browsers. The "new normal" in JS development, given that JS is going to start rapidly evolving on a feature-by-feature basis, is to use such transpilers as a standard part of your build process. This means that you should start authoring in the latest and greatest JS right now, and let tools worry about making that work in (older) browsers. No longer should you foregoe new language features for years until all previous browsers go away.

Implicit vs Explicit

It's easy to get lost in the excitement of let that it's an implicit scoping mechanism. It hijacks an existing block, and adds to that block's original purpose also the semantics of being a scope.

if (a) {
let b = a + 2;
}

Here, the block is an if block, but let merely being inside it means that the block also becomes a scope. Otherwise, if let was not there, the { .. } block is not a scope.

Why does that matter?

Generally, developers prefer explicit mechanisms rather than implicit mechanisms, because usually that makes code easier to read, understand, and maintain.

For example, in the realm of JS type coercion, many developers would prefer an explicit coercion over an implicit coercion:

When an example shows a block with only one or a few lines of code inside it, it's fairly easy to see if the block is scoped or not:

if (a) { // block is obviously scoped
let b;
}

But in more real world scenarios, many times a single block can have dozens of lines of code, maybe even a hundred or more. Setting aside the preference/opinion that such blocks shouldn't exist -- they do, it's a reality -- if let is buried way down deep in the middle of all that code, it becomes much harder to know if any given block is scoped or not.

Conversely, if you find a let declaration somewhere in the code, and you want to know to which block it belongs, instead of just visually scanning upwards to the nearest function keyword, you now need to visually scan to the nearest { opening curly brace. That's harder to do. Not a lot harder, but harder nonetheless.

It's a bit more mental tax.

Implicit Hazards

But it's not only a mental tax. Whereas var declarations are "hoisted" to the top of the enclosing function, let declarations are not treated as having been "hoisted" to the top of the block. If you accidentally try to use a block-scoped variable in the block earlier than where its declaration exists, you'll get an error:

Note: The period of "time" between the opening { and where the let b appears is technically called the "Temporal Dead Zone" (TDZ) -- I'm not making that up! -- and variables cannot be used in their TDZ. Technically, each variable has its own TDZ, and they sort of overlap, again from the opening of the block to the official declaration/initialization.

Since we had previously put the let b = .. declaration further down in the block, and then we wanted to come back and use it earlier in the block, we have a hazard -- a footgun -- where we forgot we needed to go find the let keyword and move it to the earliest usage of the b variable.

In all likelihood, developers are going to get bitten by this TDZ "bug", and they'll eventually learn from that bad experience to always put their let declarations at the top of the block.

And there's another hazard to implict let scoping: the refactoring hazard.

Let's say later, you realize the if (b > 3) part of the code needs to be moved outside the if (a) { .. block, for whatever reason. You realize you also need to grab the let b = .. declaration to move along with it.

But you don't immediately realize that the block relies on c as well -- because it's a bit more hidden down in the code -- and that c is block scoped to the if (a) { .. block. As soon as you move the if (b > 3) { .. block, now the code breaks, and you have to go find the let c = .. declaration and figure out if it can move, etc.

I could keep coming up with other scenarios -- hypothetical yes, but also extremely informed by lots of experience not only with my own but with others own real world code -- but I think you get the point. It's awfully easy to get yourself into these hazard traps.

If there had been explicit scopes for b and c, it would probably have been a little bit easier to figure out what refactoring is necessary, rather than stumbling along to figure it out implicitly.

Explicit let Scope

If I've convinced you that the implicit nature of let declarations could be a problem/hazard -- if you're not extremely careful, as well as every other developer that ever works on your code! -- then what's the alternative? Do we avoid block scoping entirely?

No! There are better ways.

Firstly, you can force yourself into a style/idiom that not only puts your let declarations at the top of the scope, but also that creates an explicit block for such scope. For example:

You'll see here I created a naked { .. } pair, and put the let b, c; declaration right at the very top, even on the same line. I'm making it as clear and explicit as possible that this is a scope block, and that it holds b and c.

If at a later time I need to move some b code around, and I go find the combined scope for b and c, it's not only easier to recognize, but easier to accomplish, that I can move the entire { let b, c; .. } block safely.

Is this perfect? Of course not. But it's better, and has less hazards and less mental tax (even by little bit) than the implicit style/idioms from earlier. I implore all of you, as you begin to use let block scoping, please consider and prefer a more explicit form over the implicit form.

Always Explicit?

In fact, I'd say being explicit is so important that the only exception I've found to that "rule" is that I like and use for (let i=0; .. ) ... It's debatable if that's implicit or explicit. I'd say it's more explicit than implicit. But it's perhaps not quite as explicit as { let i; for (i=0; ..) .. }.

There's actually a really good reason why for (let i=0; ..) .. could be better, though. It relates to scope closures, and it's very cool and powerful!

That's super cool -- it solves a very common problem developers have with closures and loops!

Note: This doesn't work in browsers yet, even those with let. The ES6 spec requires it, but at time of writing, no browsers are compliant on this particular per-iteration nuance. If you want proof, try putting the code into ES6fiddle. See...

Even More Explicit let Scope

OK, so maybe I've convinced you that explicit scopes are a bit better. The disadvantage of the above is that it's not enforceably required that you follow that style/idiom of { let b, c; .. }, which means you or someone else on your team could mess up and not follow it.

There's another option. Instead of using the "let declaration form", we could use the "let block form":

if (a) {
// make an explicit scope block!
let (b, c) {
// ..
}
}

It's a slight change, but look closely: let (b, c) { .. } creates an explicit block of scope for b and c. It's syntactically requiring b and c to be declared at the top, and it's a block that's nothing but a scope.

In my opinion, this is the best way to use let-based block scoping.

But there's a problem. The TC39 committee voted to not include this particular form of let in ES6. It may come in later, or never, but it's definitely not in ES6.

Ugh. But this isn't the first, nor the last, that something that's more preferable loses out to an inferior option.

So, are we just stuck in the previous form?

Perhaps not. I've built a tool called "let-er", which is a transpiler for "let block form" code. By default, it's in ES6-only mode, and it takes code like:

let (b, c) {
..
}

And produces:

{ let b, c;
..
}

That's not too awful, is it? It's a pretty simple transformation, actually, to get non-standard "let block form" into standard "let declaration form". After you run let-er for this transformation, you can then use a regular ES6 transpiler to target pre-ES6 environments (browsers, etc).

If you'd like to use let-er standalone without any other transpilers, only for let-based block scoping, you can optionally set the ES3 mode/flag, and it will instead produce this (admittedly hacky junk):

try{throw void 0}catch( b ){try{throw void 0}catch( c ){
..
}}

Yeah, it uses the little-known fact that try..catch has block scoping built into the catch clause.

No one wants to write that code, and no one likes the degraded performance that it brings. But keep in mind, it's compiled code, and it's only for targeting really old browsers like IE6. The slower performance is unfortunate (to the tune of about 10% in my tests), but your code is already running pretty slowly/badly in IE6, so...

Anyway, let-er by default targets standard ES6, and thus plays well with other ES6 tools like standard transpilers.

The choice to make is would you rather author code with let (b, c) { .. } style or is { let b, c; .. } OK enough?

I use let-er in my projects now. I think it's the better way. And I'm hoping maybe in ES7, the TC39 members realize how important it is to add the "let block form" into JS, so that eventually let-er can go away!

let Replaces var?

Some prominent members of the JS community and the TC39 committee like to say, "let is the new var." In fact, some have literally suggested (hopefully in jest!?) to just do a global find-n-replace of var for let.

I cannot express how incredibly stupid that advice would be.

Firstly, the hazards we mentioned above would be enormously more likely to crop up in your code, as the odds are your code is not perfectly written with respect to var usage. For example, this kind of code is extremely common:

if ( .. ) {
var foo = 42;
}
else {
var foo = "Hello World";
}

We can all probably agree it should have been written as:

var foo;
if ( .. ) {
foo = 42;
}
else {
foo = "Hello World";
}

But it's not written that way yet. Or, you're accidentally doing things like:

So, if you just blindly replace var with let in existing code, there's a pretty good chance that at least some place will accidentally stop working. All of the above would fail if let replaced var, without other changes.

If you're going to retrofit existing code with block scoping, you need to go case by case, carefully, and you need to reason about and rationalize if it's a place where block scoping is appropriate or not.

There will certainly be places where a var was used stylistically, and now a let is better. Fine. I still don't like the implicit usage, but if that's your cup o' tea, so be it.

But there will also be places that, in your analysis, you realize the code has structural issues, where a let would be more awkward, or would create more confusing code. In those places, you may choose to fix the code, but you may also quite reasonably decide to leave var alone.

Here's what bugs me the most about "let is the new var": it assumes, whether they admit it or not, an elitist view that all JS code should be perfect and follow proper rules. Whenever you bring up those earlier cases, proponents will simply strike back, "well, that code was already wrong."

Sure. But that's a side point, not the main point. It's equally hostile to say, "only use let if your scoping is already perfect, or you're prepared to rewrite it to make it perfect, and keep it perfect."

Other proponents will try to temper it with, "well, just use let for all new code."

This is equivalently elitist, because again it assumes that once you learn let and decide to use it, you'll be expected to write all new code without ever running into any hazard patterns.

I bet TC39 members can do that. They're really smart and really intimate with JS. But the rest of us are not quite so lucky.

let is the new companion to var

The more reasonable and more realistic perspective, the one I take because my primary interface with JS is through the students/attendees that I speak to, teach, and work with, is to embrace refactoring and improving code as a process, not an event.

Sure, as you learn good scoping best practices, you should make code a little better each time you touch it, and sure, your new code should be a little better than your older code. But you don't just flip a switch by reading a book or blog post, and now all of a sudden you have everything perfect.

Instead, I think you should embrace both the new let and the old var as useful signals in your code.

Use let in places you know you need block scoping, and you've specifically thought about those implications. But continue to use var for variables that either cannot easily be block scoped, or which shouldn't be block scoped. There are going to be places in real world code where some variables are going to be properly scoped to the entire function, and for those variables, var is a better signal.

In that code, let screams out at me, "hey, I'm block scoped!" It catches my attention, and I thus pay it more care. The var just says, "hey, I'm the same old function-scoped variable, because I'm going to be used across a bunch of scopes."

What about just saying let a = 10 at the top level of the function? You can do that, and it'll work fine.

But I don't think it's a good idea. Why?

First, you lose/degrade the difference in signal between var and let. Now, it's just position that signals the difference, rather than syntax.

Secondly, it's still a potential hazard. Ever had a weird bug in a program, and started throwing try..catch around things to try to figure it out? I sure do.

Block scoping is great, but it's not a silver bullet, and it's not appropriate for everything. There are places where function scoping of vars, and indeed of the "hoisting" behavior, are quite useful. These are not abject failures in the language that should be removed. They are things that should be used responsibly, as should let.

Here's the better way to say it: "let is the new block scopingvar". That statement emphasizes that let should replace varonly whenvar was already signaling block scoping stylistically. Otherwise, leave var alone. It's still doing its job pretty well!

Summary

let + var, not s/var/let/. Just frown then smirk at the next person who tells you, "let is the new var."

let improves scoping options in JS, not replaces. var is still a useful signal for variables that are used throughout the function. Having both, and using both, means scoping intent is clearer to understand and maintain and enforce. That's a big win!

About Kyle Simpson

Kyle Simpson is an Open Web Evangelist from Austin, TX, who's passionate about all things JavaScript. He's an author, workshop trainer, tech speaker, and OSS contributor/leader.

I was inspired when I first saw Addy Osmani's original ShineTime blog post. The hover sheen effect is simple but awesome. When I started my blog redesign, I really wanted to use a sheen effect with my logo. Using two HTML elements and...

As much as developers now loathe Flash, we're still playing a bit of catch up to natively duplicate the animation capabilities that Adobe's old technology provided us. Of course we have canvas, an awesome technology, one which I highlighted 9 mind-blowing demos. Another technology available...

We all joke about the days of Web yesteryear. You remember them: stupid animated GIFs (flames and "coming soon" images, most notably), lame counters, guestbooks, applets, etc. Another "feature" we thought we had gotten rid of was the marquee. The marquee was a rudimentary, javascript-like...

One of the reasons I love AJAX technology so much is because it allows us to avoid unnecessary page loads. Why download the header, footer, and other static data multiple times if that specific data never changes? It's a waste of time, processing, and bandwidth. Unfortunately...

Discussion

A quite interesting take. I personally feel that let should be the new default instead of var, but I completely agree that you can’t just blindly go and replace all vars with let for exactly the reasons you mention.

However, I’m not sure I fully understand why you think function-scoped variables should use var? I mean a function is also a block, so declaring a variable with let would mean that it’s just block-scoped to the whole function.

Same goes for the argumentation for using explicit blocks. I kinda get it that if you have a lot of code and you’re only using a variable in a small part of it, sure, then it might make sense to do that, but doing that won’t solve the problem with future refactorings moving bits out of the block because you might still have something you’re using elsewhere in the parent block.

> I’m not sure I fully understand why you think function-scoped variables should use var?

I gave two main reasons in the article:

1. Having a different keyword for function-scoped variables (var) vs block-scoped variables (let) in my opinion makes it easier to tell what the intended behavior is. If you use let everywhere, then you don’t have a different keyword to catch your attention, and thus it’s only the location that gives a signal. This is a weaker signal. Is it a massive difference? No. But it’s a lesser signal IMO, and there’s no reason why NOT to use var in those function-scoped positions, unless you’re of the “cult” of “let is the new var”. :)

2. It’s too easy to accidentally wrap code in blocks (even temporarily) that can create unexpected issues with let where var would have continued to work the same. The main case where I run into this issue is when I’m debugging and putting try..catch into my code.

Again, I don’t understand why people are in such a hurry to get rid of var? It’s not bad, and it’s not poorly designed. let is useful to add to the toolbox available to us, but it has different behavior, and vars behavior is, IMO, sometimes preferable.

Perhaps this is just because I’m more used to languages which have block level scoping :) I always tend to think of variables as block level even when not using let.

For #1, I see var as an extra case I have to pay attention to. Instead of just thinking that all my variables are block level scoped, I also have to pay attention to which keyword was used to declare it.

The second case you mention was never an issue for me, as I kind of automatically just move things into the correct blocks even when using var purely out of habit.

I can see how for me this is probably because my background before JavaScript was languages which only have block level scoping, and I still use them. I can understand your point of view though, especially if thinking of people who don’t have a lot of experience in languages that have block scoping. The case with try-catch (and other similar things) is definitely something I’ve seen happen.

Evgeniy

So ‘let’ buried deep in block will make that block non-reusable and non-refactorable.
Cool! Awesome! Another good thing implemented in JS with crappy side-effect. Is it made specially, just to not break JS-tradition to make everything a little bit crappy?

Thanks for head-up. I agree, let (a,b) { } could be most explicit variant of scope. It was too good for this language, that’s why they declined it.

I can’t begin to guess why someone decided to add “Temporal Dead Zone” to our vocabulary, other than that it probably followed a 17-hour binge-watch of ST:TNG accompanied by too much hipster brew.

Those of us who have been programming in Java, C++, and the like for the last two decades or so just call those “the place where I haven’t yet declared the variable I want to use.” I think life would have been better for all concerned if braces defined block scope and we could just be done with it. (Maybe not better for Brendan back in the day, but better now, IMHO.)

Really appreciate your article on this and it gave me exactly what I needed to make a decision on how I’ll be using let. Let-er looks interesting as well. Do you have any idea how I could get that working with Ember’s build process? Thanks!

tic

I think that it does make sense to replace all vars with lets. It’s true that it might break existing code, but coming from a C++/C# background, it makes sense and behaves much more like every other language

Explicit typing however is more verbose and I do not quite think it brings as much benefit. I definitely am in the ‘let and var’ group :). Let still doesn’t allow for global variables – something var does really well.

But declaring explicit blocks seems just too much. Great post all the same.

luke

Why not make an exception for “try catch block”, so that it will not create new scope, I mean:

This is an excellent post and you made your case quite well and provided a good history (I was looking at a video that included the “old”, disallowed explicit let syntax and wondering why it didn’t work until I read your answer).

I also think your caution against a blind search and replace is quite prudent. On balance, however, I have to side with the reader who mentioned that it’s wise to use let as extensively as possible going forward, but this is only because I am far more steeped in Java / C++ / C / C# scoping rules (which all have block scope), so to me it seems natural. But I can see where, if you’re steeped in the JS world as it is, the movement to block scope can appear to be more “elitist”. If it makes you feel better, however, I think the JS guys are the cool kids now. :)