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:
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 '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.
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:
// 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:
?? 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:
?? 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:
If you have other examples of ways in which ASI can cause problems, let me know in the comments.