Topics

Jump to a topic.

Search questions

Find a question by title, tag, or summary.

How does automatic semicolon insertion work, and when does it bite?

Questions in this chapter (5)
  • Draft
  • medium
  • Askedsometimes
  • Verified October 2026

Answer

ASI is a parser repair rule, not "newlines end statements". The spec defines three cases:

  1. Offending token. The parser reads left to right. If it meets a token the grammar doesn't allow there, it inserts a ; before that token if (a) there's a line terminator between it and the previous token, (b) the token is }, or (c) the previous token is ) and the ; would end a do…while.
  2. End of input. If the token stream ends and the program isn't complete, a ; is added.
  3. Restricted productions. After a few keywords, [no LineTerminator here] forbids a newline: return, throw, break, continue, yield, postfix ++/--, and the => of an arrow function (and async before function). A line break there makes the parser insert a ; even though the next token would have been legal.

Two overriding limits: a ; is never inserted if it would become an empty statement, or one of the two semicolons in a for(;;) header.

When it bites:

  • return followed by a newline returns undefined; the value on the next line is dead code. Keep the value, or an opening (/{, on the return line.
  • A line starting with (, [, a template literal, +, -, or / is a legal continuation, so no ; is inserted and the two lines merge into a call, property access, tagged template, or binary expression. Use a semicolon on the previous line, or in "no-semicolon" code style, a leading ; on these lines.
  • a newline ++b parses as a; ++b, because postfix ++ is restricted.

The reverse also exists: ASI can't fix real syntax errors, so for (let i = 0⏎i < 3⏎i++) is a SyntaxError.

Example

const run = (source) => {
  try {
    return new Function(source)();
  } catch (error) {
    return error.name;
  }
};

console.log(run('return\n{ ok: true }')); // → undefined
console.log(run('let a = 1, b = 2\nlet r = a\n++b\nreturn [a, b, r]')); // → [ 1, 3, 1 ]
console.log(run('const s = "a"\n[1].map(String)')); // → TypeError
console.log(run('const s = "a"\n;[1].map(String)\nreturn s')); // → a
console.log(run('throw\nnew Error("x")')); // → SyntaxError
console.log(run('for (let i = 0\ni < 3\ni++) {}')); // → SyntaxError
console.log(run('return /*\n*/ 5')); // → undefined
console.log(run('do {} while (false) return 3')); // → 3

Pitfalls

  • Believing "JavaScript ends a statement at a newline". It doesn't; it inserts a ; only where the grammar would otherwise fail (or at a restricted production).
  • Writing return and putting the object, array, or parenthesised expression on the next line. A bare return is a complete statement.
  • Multi-line comments: a /* … */ comment that contains a line break counts as a line terminator, so return /*⏎*/ 5 returns undefined.
  • Skipping semicolons without a plan. A statement beginning with (, [, `, +, -, or / silently joins the line above it. Teams that omit semicolons need a lint rule or the leading-; habit; teams that keep them still need to know the return, throw, break, continue, and ++ cases.

Follow-ups

Is the ; after do…while (cond) ever optional?
Yes. Rule 1(c) inserts it after ) in a do…while, so do {} while (false) foo() on one line is valid. It's a special case the spec lists separately.
Why isn't ASI applied in for (a⏎b⏎c)?
The override rule forbids a ; that would become one of the two header semicolons, so the parser reports a SyntaxError instead.
Are semicolons required to avoid ASI bugs?
Not required, but they remove the guesswork from lines that start with ( or [. Restricted productions (return⏎value) still need care either way.

Exercises

Predict the output

let a = 1
let b = 2
a
++
b
console.log(a, b)

Fix the bug

const names = ['Ada', 'Grace']
[0, 1].forEach((i) => console.log(names[i]))