JavaScript and ASI, oh my!

JavaScript and ASI, oh my!

Coding in JavaScript without semicolons seems to be divisive, with some engineers lamenting the lack of semicolons during code reviews, and others saying it doesn't matter we don't need them.

While it's true that in JavaScript you can often get away with not using a semicolon, you probably need to know how, why and when you can do this. Understanding how the ASI process works, and some of its pitfalls, will help us all write better, less ambiguous, and more readable code.

JavaScript’s ASI feature can be unpredictable, and while many times your code will work fine without semicolons, some edge cases can lead to subtle and hard-to-debug bugs.

Declaration: I've read and used resources from the following places to gather information while writing this piece, and when I finished writing, I asked chatGPT to proof read, check for errors, and make any rants and rambles easier to read and digest - though it's still reeeeeaaalllly long.

These are also useful and interesting reading if you want more in depth information about ASI and it's foibles, whichever side of the semicolon fence you reside.


What is ASI?

In the simplest terms, Automatic Semicolon Insertion (ASI) tries to insert semicolons in places where they seem to be missing

For example, if the parser can't make sense of a series of two tokens in source code, it'll add a semicolon between them automatically to make the code grammatically correct. This is ASI." from JavaScript Grammar - semicolons and ASI

BUT, this doesn't always go to plan and whammo, you get errors that are super tricky to debug.

ASI can also go wrong if your code is badly formed, adding random; semicolons slap; bang where; you don't want them.

;


What are the rules?

ESLint documentation has some nice, clear explanations of the ASI rules, along with code examples for both the no-semicolon and yes-semicolon folks. Here are two of the pages explaining ESL rules that are concerned with ASI:

ESL Rule: no-unexpected-multiline

ESLRule: semi

A summary of what you'll find on those pages:

The rules for ASI (Automatic Semicolon Insertion) are simple: a newline character always ends a statement, just like a semicolon, except in these cases:

  • The statement ends with an unclosed paren, array, or object literal, or another invalid statement-ending character (e.g., . or ,).
  • The line ends with -- or ++, meaning the next token will be decremented/incremented.
  • It's a for(), while(), do, if(), or else without a {.
  • The next line starts with [, (, +, *, /, -, ,, ., or another binary operator that connects tokens in a single expression.


?? The 'Return Statement Ambiguity' Pitfall

Pretty sure we've all done this at some point, thinking code looks prettier, clearer, and more readable by separating everything out into its own lines of code. Just an engineer happily putting an opening brace on its own line after a return.

Question: Where do you think ASI will add the semicolon here? I'm pretty sure 99% of you will get this one correct; it's one of the more obvious ASI pitfalls of doom.

function getAThingyMajig() {
  return
    {
       thisThingyMajig: 1
     }
}        

Answer: ASI will add a semicolon after 'return', resulting in unreachable code.

Correct code

// for the no-semicolon peeps

function getAThingyMajig() {
  return {
       thisThingyMajig: 1
     }
}


// for the yes-semicolon peeps, though it's really not needed here as 
// ASI will do its job, you mark the end of the return function

function getAThingyMajig() {
  return {
       thisThingyMajig: 1
     };
}        


?? The 'Fleetwood Mac' Pitfall - Chaaaaaaain, keep us together!

ASI doesn't always add semicolons, so if you are a serial leaver outer of semicolons and expect ASI to add semicolons in chained operations or method calls, you're in for a treat!

In the example below, ASI won't add a semicolon after 'let fish = 1' because it sees the [ and assumes you want to continue the expression with the array.

let fish = 1

[1, 2, 3].forEach(n => fish += n)        

This is what ASI thinks you're trying to do:

let fish = 1[1, 2, 3].forEach(n => fish += n)        

It's an easy fix though, just pop a little old semicolon in there....

let fish = 1;

[1, 2, 3].forEach(n => fish += n);        


?? The 'it hurts my eyes don't do this!' Pitfall

Leaving out semicolons when decrementing or incrementing should be okay, unless you're a crazy person who writes code that makes code reviewers yell "OW MY EYES!", which is, hopefully, none of you lovely people reading this dissertation of doom on semicolons.

  • When ++ or -- is used before the variable, ASI handles it by directly operating on the next variable in the line, i.e. ++x or --x
  • When ++ or -- is used after the variable, ASI behaves similarly, as long as the operator is part of the same statement, i.e. x++ or x--
  • When ++ or -- are on a line all by themselves because you're writing psychotic code that will make code reviewers cry, then ASI may run into problems and create something funky. Thankfully, I've never seen anyone writing code this horrible and hope I never do:

let x = 1
x
++
x        

In the example above, ASI will have fun with your code. Semicolons are going to semicolon all over this, along with changing the meaning and creating some unexpected 'interesting' results, like so:

let x = 1;
x++; 
++x;        

Here's how it should look if you want to remove any ambiguity and be nice to your resident code reviewer:

let x = 1;
x++;         


?? The 'ASI-induced confusion' Pitfall

Omitting semicolons in for, while, or if statements may cause confusing errors and side effects when ASI tries to do its thing.

// Example of unintended side effects due to ASI

function increment() {
    return 
    {
        value: 1  
    } 
}

for (let i = 0; i < 5; i++) 
    increment()

while (false)
    console.log("This will never run")

if (true)
    console.log("This is true")
else
    console.log("This is false")        

Explanation of Problems:

  1. increment() function: The return statement does not behave as expected because ASI will insert a semicolon after return, making it return undefined. The object { value: 1 } is not returned.
  2. for loop: Without braces, the loop executes only the increment() call, which can lead to confusion if additional logic was intended.
  3. if-else statement: ASI can insert semicolons incorrectly, breaking the expected flow of if-else.

// Correct usage with explicit semicolons to avoid ASI issues

function increment() {
    return {
        value: 1
    }; // Correctly return the object
}

for (let i = 0; i < 5; i++) {
    increment(); // Now safely executed within block
}

while (false) {
    console.log("This will never run");
}

if (true) {
    console.log("This is true");
} else {
    console.log("This is false");
}        

Why this is better:

  1. Semicolon after return statement: Ensures the object is returned as intended.
  2. Block statements for for, while, and if-else: Avoids ambiguity by grouping the logic within braces, reducing the risk of ASI-induced confusion.
  3. Semicolons at the end of statements: Ensures predictable behaviour that avoids unexpected loops or logic breaks.


?? The 'where does it end?' Pitfall

If you concatenate multiple scripts without semicolons, ASI can create issues if it's not sure where one script ends and another begins. For example, can you figure out how this will go totally pear shaped?

// First script: Function definition
function multiply(a, b) {
    return a * b
}

// Second script: Immediately Invoked Function Expression (IIFE)
(function() {
    console.log('This is an IIFE')
})()

// Third script: Variable assignment
let result = multiply(2, 3)
console.log(result)        

Because there is no semicolon after the multiply function, ASI will get confused and it may think the second script is being invoked immediately without any separation from the first script, and this will generate an unexpected error. ASI, in its wisdom, may interpret the code like this:

return a * b(function() {
    console.log('This is an IIFE')
})()        

A safer version of this code would look like this:

// First script: Function definition with a semicolon
function multiply(a, b) {
    return a * b;
}

// Second script: Immediately Invoked Function Expression (IIFE)
(function() {
    console.log('This is an IIFE');
})();

// Third script: Variable assignment with a semicolon
let result = multiply(2, 3);
console.log(result);        

Why this is better:

  1. Semicolon after multiply function: Prevents ASI from interpreting the following IIFE as an argument to multiply().
  2. Semicolons between scripts: Each block of code is clearly separated, preventing misinterpretation by ASI.
  3. Predictable concatenation: The scripts are concatenated safely, and each one works as expected without interfering with others.


?? The 'nope, not throwing that' Pitfall

Let's finish on a pitfall that's easy to avoid.

When using throw statements, always end the statement with a semicolon. Omitting it can cause ASI to break your code, resulting in unexpected syntax errors, especially since throw requires an expression immediately after it.

What do you think ASI will do with this code?

function throwError() {
    throw "Error occurred"  // No semicolon after the throw statement
    // This line causes ASI to break the code
    console.log("This will never run")
}

try {
    throwError()
} catch (error) {
    console.log(error)
}        

ASI will automatically insert a semicolon after the throw statement. This leads to a syntax error because ASI inserts a semicolon too early, essentially converting the code into:

throw;   // Automatically inserted semicolon here by ASI
"Error occurred"        

Using explicit semicolons will help to avoid ASI craziness:

function throwError() {
    throw "Error occurred";  // Semicolon added here to avoid ASI issues
    console.log("This will never run"); // This line is unreachable but correct
}

try {
    throwError();
} catch (error) {
    console.log(error);  // Outputs: "Error occurred"
}        

Why this is better:

  1. Semicolon after throw statement: Prevents ASI from incorrectly inserting a semicolon after throw, ensuring the string "Error occurred" is properly thrown.
  2. Clear execution flow: With the semicolon, the throw statement behaves as expected and no unwanted syntax errors occur.


If you have other examples of ways in which ASI can cause problems, let me know in the comments.

要查看或添加评论,请登录

Amanda Jackson的更多文章

  • Bridging the Communication Gap: Strategies for Neurodivergent and Neurotypical Interactions

    Bridging the Communication Gap: Strategies for Neurodivergent and Neurotypical Interactions

    Background My preferred way to communicate is through reading and writing. It’s much easier for me to clearly express…

    1 条评论
  • Build Self Mastery - No More Zero Days

    Build Self Mastery - No More Zero Days

    This mental health awareness month, I thought it may be useful to share a technique I use myself that has helped me…

  • Better Meetings

    Better Meetings

    How to make meetings better: Issues, Solutions, Ideas The issues with meetings Focus time is reduced Throughout the…

  • Communication in a diverse world

    Communication in a diverse world

    This article is based on notes I wrote that serve as my reference for a neurodiversity presentation. I've taken the…

    1 条评论
  • Senior Architects Wanted!

    Senior Architects Wanted!

    Do you have a wealth of experience with .NET, SQL Server, BizTalk? If so, Nigel Frank Ltd (recruiters) are looking for…

社区洞察

其他会员也浏览了