Today I wrote my first real for-loop in JavaScript in months. As you probably know, the syntax of this loop involves (at least) two semicolons. I have been writing all my JavaScript without semicolons for some time now and this example made me think about the thing that allows me to do so: Automatic semicolon insertion (ASI). This is a mechanism implemented by JavaScript engines to accept source code without semicolons and automatically insert them where possible. Of course, the for-loop still requires semicolons, but through this mechanism they become optional pretty much anywhere else. I have not looked through the exact rules of ASI, but rather looked for some edge cases where it goes wrong. In that context wrong can mean two different things. First, it is possible to change what we think a program should be doing by automatically inserting semicolons. Second, it is possible to have a program that looks perfectly valid but throws weird runtime errors when semicolons are automatically inserted. The reason in both cases is the same: The JavaScript engine inserts semicolons in different places than we intended them to be. Here are three examples where exactly this happens:

Function return statement

function a() {
	return
	"Nope, I am sorry!"
}

a() // undefined

// After ASI
function a() {
	return;
	"Nope, I am sorry!";
}

In this case the engine considers the return keyword as its own statement and inserts a semicolon after it. Anything on the following lines will be standalone expressions that I think will not even be evaluated. If you are a React user, you probably know an easy fix for this. The render functions of React components return JSX and it is often convenient to have that start on a new line after return. To prevent ASI from inserting the semicolon after return, we can simply wrap the return value with parentheses starting on the same line as the return keyword.

Line starting with parentheses

const b = 1
(function() { console.log('Old school module pattern!') })()

// Uncaught TypeError: 1 is not a function

// After ASI
const b = 1
(function() { console.log('Old school module pattern!') })();

I am not sure where this kind of thing could be a problem, but it is another example where the engine is left alone in ambiguity. You might as well call an actual function like this (really…?).

Line starting with angle brackets

const c = 3
[1,2].map(e => e*2)

// Uncaught TypeError: Cannot read property 'map' of undefined

// After ASI
const c = 3
[1,2].map(e => e*2);

The error message of this one is particularly confusing. Why should map be called on undefined? I have not checked this, but my guess is the following:

const c = 3
[1,2].map(e => e*2)
// Angle brackets are interpreted as property access on '3'
const c = 3[1,2].map(e => e*2)
// Property key '1,2' is evaluated as comma expression, resulting in 2
const c = 3[2].map(e => e*2)
// '3' is boxed to Number object
// Property '2' does not exist on this object, resulting in undefined
const c = undefined.map(e => e*2)

These and all other examples I have found so far include a line break. In these cases a line break intuitively means the same thing as a semicolon for us, but not for the engine. So I think if we are careful around newlines, omitting semicolons should be fairly safe. I like the aesthetics of code without them and have yet to run into a real problem, so I guess I will keep it like that for now.