The Dangers of JavaScript's Automatic Semicolon Insertion

Colin J. Ihrig

Although JavaScript is very powerful, the language's fundamentals do not have a very steep learning curve. Prior to the explosion of web applications, JavaScript was thought of as a toy language for amateur programmers. Some of JavaScript's features were specifically designed to cater to beginners. One such feature is automatic semicolon insertion. Automatic semicolon insertion is also one of JavaScript's most controversial features.

JavaScript's syntax borrows heavily from C and Java. Programmers that are familiar with these languages are accustomed to semicolon terminated statements. JavaScript statements are also terminated by semicolons, but unlike C and Java, these semicolons are not always required. In an effort to "help" programmers, the JavaScript interpreter will actually insert omitted semicolons where it deems necessary. Unfortunately, automatic semicolon insertion, which was intended to act as a crutch for programmers, can actually introduce difficult to find bugs.

Section 7.9.1 of the ECMAScript 5.1 Standard specifies rules governing automatic semicolon insertion. To understand the rules, you should first understand the concept of tokens. Most of the rules involve inserting a semicolon at the end of a line of code ― referred to as a LineTerminator token. Because automatic semicolon insertion rules are related to line breaks, whitespace can effect the execution of JavaScript programs. Many common languages (C, Java, HTML) allow developers to ignore whitespace. Developers who are familiar with these languages can run into problems in JavaScript due to this assumption.

It is important to recognize the scenarios where automatic semicolon insertion is applied. There are also a number of scenarios where semicolons are not automatically inserted. The following sections describe the rules for inserting (or not inserting) semicolons automatically.

LineTerminator, Closing Braces, and End of Stream

The JavaScript interpreter will insert a semicolon between two statements when they are separated by a LineTerminator or a } token. A semicolon will also be inserted, if needed, at the end of the input stream. The following if statement is valid JavaScript.

if (i === 0) {
  foo = 1
  bar = 2 } baz = 3

A semicolon is inserted between the foo and bar assignment statements because they are separated by a LineTerminator. Another semicolon is inserted after the bar assignment because the next token is a closing curly brace. A final semicolon is inserted after the baz assignment because the end of the input stream has been reached. After semicolon insertion, the if statement looks like this:

if (i === 0) {
  foo = 1;
  bar = 2; } baz = 3;

return, throw, continue, and break Statements

If a LineTerminator token is encountered immediately after a return, throw, continue, or break token, a semicolon is automatically inserted. This means that labels in continue and break statements must be specified on the same line as the respective continue or break token. Similarly, expressions in return and throw statements must begin on the same line as the return or throw token. For example, the following return statement does not exhibit the behavior that the developer likely intended.

return
a + b;

The developer most likely intended to return the result of the expression a + b. However, when this return statement is parsed by the interpreter, it is transformed to look like the following code. In this case, the return value is undefined and the a + b expression becomes unreachable code.

return;
a + b;

return statements that return object literals are potentially the most common victims of semicolon insertion related bugs. Object literal syntax lends itself well to being split across multiple lines. This is especially true for large objects. For example, the following function returns an undefined value.

function getObject() {
  return
  {
    foo: 1
    // many more fields
  };
}

Postfix Operators

The postfix operators ++ and -- must appear on the same line as their operand. If a LineTerminator occurs between the operand and the operator, then a semicolon will be inserted by the interpreter. These mistakes are uncommon. For example, a developer is unlikely to write i++ on multiple lines.

for Statements

The header of a for loop must always contain two semicolons. According to the specification, semicolons are never automatically inserted into the header of a for loop. This means that the programmer is responsible for including both semicolons. For example, the following loops are valid JavaScript:

for (var i = 0; i < 5; i++) {
  // loop body
}

for (; ;) {
  // loop body
}

for (var i = 0; i < 5;
i++) {
  // loop body
}

However, the following loops are not valid because the missing second semicolon is not automatically inserted.

for (var i = 0; i < 5
i++) {
  // loop body
}

for ( ;
) {
  // loop body
}

Because the missing semicolons are not inserted, the loops look like this to the interpreter:

for (var i = 0; i < 5 i++) {
  // loop body
}

for (;) {
  // loop body
}

Empty Statements

Semicolons are also never inserted when the resulting statement would be the empty statement ― a statement that consists of only a semicolon. The following if-else statement is invalid. The interpreter will not insert a semicolon in the if clause because the resulting statement would be empty.

if (i === 5)
  // no semicolon will be inserted here
else
  foo = 0;

A More Complicated Example

All semicolons have been removed from the following example. In this case, it can be more difficult to determine the semantics of the code. What will the value of foo be at the end of the example?

var foo
var bar
var baz = function(data) {
  return data +
  1
}

bar = 1
foo = bar + baz
(bar + bar) + baz(bar)

Let's analyze the code. The first three lines declare the variables foo, bar, and baz. baz is a function that increments its data argument by one. Because the expression data + 1 begins on the same line as the return token, baz returns the expected value. The bar assignment statement is straightforward. The foo assignment is trickier. A semicolon is not inserted between the last two lines because the opening parentheses indicates a function call that began on the previous line. Therefore, the foo assignment actually looks like this:

foo = bar + baz(bar + bar) + baz(bar);

Now it is fairly simple to compute foo. The final value of foo is six.

Things to Remember

  • If the programmer leaves out a semicolon, the JavaScript interpreter will insert it automatically in some circumstances.
  • Automatic semicolon insertion can introduce bugs which are difficult to locate because whitespace changes semantics.
  • Programmers should only rely on automatic semicolon insertion if tooling, such as a linter, is setup to help catch potential issues.

Appendix

The following excerpt is taken directly from Section 7.9.1 of the standard, which describes the rules for automatic semicolon insertion. The 'restricted productions' mentioned in Rule #3 relate to postfix operators and the continue, break, return, and throw statements.

Begin Excerpt

There are three basic rules of semicolon insertion:

  1. When, as the program is parsed from left to right, a token (called the offending token) is encountered that is not allowed by any production of the grammar, then a semicolon is automatically inserted before the offending token if one or more of the following conditions is true:
    • The offending token is separated from the previous token by at least one LineTerminator.
    • The offending token is }.
  2. When, as the program is parsed from left to right, the end of the input stream of tokens is encountered and the parser is unable to parse the input token stream as a single complete ECMAScript Program, then a semicolon is automatically inserted at the end of the input stream.
  3. When, as the program is parsed from left to right, a token is encountered that is allowed by some production of the grammar, but the production is a restricted production and the token would be the first token for a terminal or nonterminal immediately following the annotation ―[no LineTerminator here]‖ within the restricted production (and therefore such a token is called a restricted token), and the restricted token is separated from the previous token by at least one LineTerminator, then a semicolon is automatically inserted before the restricted token.

However, there is an additional overriding condition on the preceding rules: a semicolon is never inserted automatically if the semicolon would then be parsed as an empty statement or if that semicolon would become one of the two semicolons in the header of a for statement (see 12.6.3).

End Excerpt