For and against `let`

By  on  

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.

Function Scope

Let's briefly review the basics of function scoping -- if you need more indepth coverage, check out my "You Don't Know JS: Scope & Closures" book, part of the "You Don't Know JS" book series.

Consider:

foo();    // 42

function foo() {
    var bar = 2;
    if (bar > 1 || bam) {
        var baz = bar * 10;
    }

    var bam = (baz * 2) + 2;

    console.log( bam );
}

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:

function foo() {
    var bar, baz, bam;

    bar = 2;

    if (bar > 1 || bam) {
        baz = bar * 10;
    }

    bam = (baz * 2) + 2;

    console.log( bam );
}

foo();  // 42

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:

for (var i=0; i<10; i++) {
    // ..
}

That is also extremely common. Another example that's fairly common:

var a, b;

// other code

// later, swap `a` and `b`
if (a && b) {
    var tmp = a;
    a = b;
    b = tmp;
}

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:

function foo() {    // <-- Look ma, no named parameters!
    // ..

    {
        var x = arguments[0];
        var y = arguments[1];

        // do something with `x` and `y`
    }

    // ..
}

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:

function foo() {
    a = 1;                  // careful, `a` has been hoisted!

    if (a) {
        var a;              // hoisted to function scope!
        let b = a + 2;      // `b` block-scoped to `if` block!

        console.log( b );   // 3
    }

    console.log( a );       // 1
    console.log( b );       // ReferenceError: `b` is not defined
}

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:

var a = "21";

var b = a * 2;          // <-- implicit coercion -- yuck :(
b;                      // 42

var c = Number(a) * 2;  // <-- explicit coercion -- much better :)
c;                      // 42

Note: To read more on this side-topic of implicit/explicit coercion, see my "You Don't Know JS: Types & Grammar" book, specifically Chapter 4: 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:

if (a) {
    b = a + 2;      // ReferenceError: `b` is not defined

    // more code

    let b = ..

    // more code
}

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.

Consider:

if (a) {
    // more code

    let b = 10;

    // more code

    let c = 1000;

    // more code

    if (b > 3) {
        // more code

        console.log( b + c );

        // more code
    }

    // more code
}

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:

if (a) {
    // more code

    // make an explicit scope block!
    { let b, c;
        // more code

        b = 10;

        // more code

        c = 1000;

        // more code

        if (b > 3) {
            // more code

            console.log( b + c );

            // more code
        }
    }

    // more code
}

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!

{ let i;
    for (i=1; i<=5; i++) {
        setTimeout(function(){
            console.log("i:",i);
        },i*1000);
    }
}

That code will, like its more typical var counterpart, not work, in that it'll print out i: 6 five times. But this code does work:

for (let i=1; i<=5; i++) {
    setTimeout(function(){
        console.log("i:",i);
    },i*1000);
}

It'll print out i: 1, i: 2, i: 3, etc. Why?

Because the ES6 specification actually says that let i in a for loop header scopes i not only to the for loop, but to each iteration of the for loop. In other words, it makes it behave like this:

{ let k;
    for (k=1; k<=5; k++) {
        let i = k; // <-- new `i` for each iteration!
        setTimeout(function(){
            console.log("i:",i);
        },i*1000);
    }
}

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...

for-let scoping

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!

Either way, explicit block scoping is better than implicit. Please block scope responsibly.

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:

b = 1;

// ..

var b;

Or you're accidentally relying on non-block-scoped closure in loops:

for (var i=0; i<10; i++) {
    if (i == 2) {
        setTimeout(function(){
            if (i == 10) {
                console.log("Loop finished");
            }
        },100);
    }
}

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.

function foo() {
    var a = 10;

    if (a > 2) {
        let b = a * 3;
        console.log(b);
    }

    if (a > 5) {
        let c = a / 2;
        console.log(c);
    }

    console.log(a);
}

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.

Oops:

function foo() {
    try {
        let a = 10;

        if (a > 2) {
            let b = a * 3;
            console.log(b);
        }
    }
    catch (err) {
        // ..
    }

    if (a > 5) {
        let c = a / 2;
        console.log(c);
    }

    console.log(a);
}

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 scoping var". That statement emphasizes that let should replace var only when var was already signaling block scoping stylistically. Otherwise, leave var alone. It's still doing its job pretty well!

Summary

Block scoping is cool, and let gives us that. But be explicit about your block scopes. Avoid implicit let declarations strewn about.

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!

Kyle Simpson

About Kyle Simpson

Kyle Simpson is a web-oriented software engineer, widely acclaimed for his "You Don't Know JS" book series and nearly 1M hours viewed of his online courses. Kyle's superpower is asking better questions, who deeply believes in maximally using the minimally-necessary tools for any task. As a "human-centric technologist", he's passionate about bringing humans and technology together, evolving engineering organizations towards solving the right problems, in simpler ways. Kyle will always fight for the people behind the pixels.

Recent Features

  • By
    fetch API

    One of the worst kept secrets about AJAX on the web is that the underlying API for it, XMLHttpRequest, wasn't really made for what we've been using it for.  We've done well to create elegant APIs around XHR but we know we can do better.  Our effort to...

  • By
    Write Simple, Elegant and Maintainable Media Queries with Sass

    I spent a few months experimenting with different approaches for writing simple, elegant and maintainable media queries with Sass. Each solution had something that I really liked, but I couldn't find one that covered everything I needed to do, so I ventured into creating my...

Incredible Demos

  • By
    FileReader API

    As broadband speed continues to get faster, the web continues to be more media-centric.  Sometimes that can be good (Netflix, other streaming services), sometimes that can be bad (wanting to read a news article but it has an accompanying useless video with it).  And every social service does...

  • By
    Reverse Element Order with CSS Flexbox

    CSS is becoming more and more powerful these days, almost to the point where the order of HTML elements output to the page no longer matters from a display standpoint -- CSS lets you do so much that almost any layout, large or small, is possible.  Semantics...

Discussion

  1. 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.

    • Jani-

      > 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.

      function foo() {
         let a = 2;
         let b = bar( a * 3 );
      
         b = a * 10;
         bar( b / 2 );
      }
      

      vs.

      function foo() {
         let a = 2;
      
         try {
            let b = bar( a * 3 );
         }
         catch (e) {
            // ..
         }
      
         b = a * 10;     <-- Oops, error here now that wasn't before
         bar( b / 2 );
      }
      

      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.

      That’s why I’m in the “let and var” cult.

    • 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.

  2. 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.

    Now I wonder if they screwed up classes in some way also…

  3. stephen

    wouldn’t it have been better to write it like this:

    var foo = "Hello World";
    
    if ( .. ) foo = 42;
    
  4. Brook Monroe

    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.)

  5. Your example with the loop works for me when I replace var with let:

    for (let i=0; i<10; i++) {
        if (i == 2) {
            setTimeout(function(){
                if (i == 10) {
                    console.log("Loop finished", i);
                }
            },100);
        }
    }

    This is because the function is within the for block scope?

  6. So, why don’t we use functions to simulate block scope?, is way more explicit. e.g:

    function test(){
      var c;
      ...
      {
          let a = 3, b = 4;
          c= a+b;
       }
       ...
       return c;
    }
    

    instead:

    function addValues(a,b){
         return a+b;
    }
    
    function test(){
     var c;
     ...
      c = addValues(3,4);
     ...
      return c;
    }
    
  7. Dean

    As of today, the let code that fixes the closure problem in a for loop *does* work in Google Chrome – woohoo!

  8. Hey man,

    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!

  9. 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

  10. Great post Kyle!

    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.

  11. luke

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

    try {
        let a = 0;
    }
    console.log(a) // wrong
    
    would be equivalent to this:
    
    let a;
    try {
        a = 0;
    }
    
    console.log(a) // OK
    
    

    Similar thing is in python AFAIK.

  12. para

    I hate the use of foo and bar and baz and bam…. use some real examples!!!

  13. 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. :)

  14. Sean

    Um, let is not new to ES6. It’s listed as something ambiguous and dangerous in Crockford’s “Javascript: The Good Parts”, and it’s been in JS for ages.

    • I believe you are mistaken. JavaScript most definitely did not have let in it until ES6, that much I am sure. I know that spidermonkey had an experimental version of it a decade ago, but it was only in that one engine until ES6. Also, I don’t recall Crockford talking about it in his book, but even if he did, he was definitely not talking about something that was part of the standard at the time.

  15. Craig P Hicks

    How about a transpiler to convert existing code into canonical form, such that each variable is declared with “let” at the top of it’s minimal possible scope , and declarations are separated from assignment as necessary to avoid your try-catch mistake. Optionally, the transpiler could also introduce new scopes with brackets, with an ad hoc weighting to balance the number of scopes (less is better) with size of scopes (smaller is better).
    That could be run on code before human refactoring, to reduce the risks you mentioned. Similar to pretty-printing – in a way.
    Such a transpiler could be made without any new science (and may already have been made). According to your arguments about the dangers of refactoring, such a transpiler could be used before refactoring to reduce human error.

  16. Ed

    When ever new functions or options are entering a language, first question for me is, does it solve my problem? With let, it doesnt solve any of my problems , because i have no problems with var. Why is that?

  17. So I think I call it that: “let is the new var, and var is the exception from that.”

    let is how C# worked from the start. In a C#/JavaScript project, let is easier to understand because it just does the same as a variable in C# does. var could still be used in cases where the variable should also be accessible outside its block. But that should be the exception because it requires the reader to fully understand the special behaviour of var, which most just don’t.

  18. How let c = 3 is hoisted as we know that we cannot write:

    let c;
    c = 3;
    
  19. C

    >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.

    what in the actual flurry did I just read. C and Java and other such language devs have been doing just fine with block scoping. The only reason you shouldn’t find and replace var=>let is because of legacy code that counts on function scoping to work.

    your justification with the try catch block just reads like another attempt at grasping straws to justify the idiocy of var in 2020.

    the only situation I would think of using var, would be when I’m lazy. It happened to me before,

    Imagine this situation:

    if(true) {
    let a = 123;
    }
    

    and then later I change the code to the following, without being careful with the scope

    if(true) {
    let a = 123;
    }
    if(a == 123) {
      console.log(a); //error
    }
    

    but if I used var, it would work fine, without needing to change the code to:

    let a = "";
    if(true) {
    a = 123;
    }
    if(a && a == 123) {
      console.log(a);
    }
    

    THe above is much more robust, using var is asking for trouble. Can’t change my mind on that.

    • Eric

      Absolutely agree. I’ve been doing C and C# for ages now, and never had any problems with block scope.
      And of course a function is a scope too – it has curly braces!

      As of (around) 2018, in any new JS project I would not dream of ever using ‘var’ again.

  20. jeannette

    So does this thing work let (a,b) { } now?

    And well, you say not to use let instead of var at the top of a function, but we most likely want to use “const” anyway, so if we use “const” which is like “let” but read-only, then it doesn’t make much sense to use “var” instead of “let” because it’s not coherent

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!