Skip to main content

JavaScript Statement & Expression

JavaScript Semicolons

Written by Updated

JavaScript semicolons are used to separate statements.

JavaScript statements end with a semicolon - and if you leave it off, the engine usually puts one in for you. "Usually" is the problem. Automatic semicolon insertion (ASI) has a short list of rules, and every one of the famous semicolon bugs comes from one of them. Learn the list and the whole debate becomes boring, which is the goal.

Try it

Both functions look like they return an object. Run it and look at the output.

html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>The return bug</title>
</head>
<body>
<pre id="out"></pre>
<script>
  function withBrace() {
    return {
      ok: true
    };
  }

  function withNewline() {
    return
    {
      ok: true
    };
  }

  document.getElementById("out").textContent =
    "withBrace():   " + JSON.stringify(withBrace()) + "\n" +
    "withNewline(): " + JSON.stringify(withNewline());
</script>
</body>
</html>

withNewline() returns undefined. The engine saw return followed by a line break, inserted a semicolon right there - return; - and the object below became an unreachable block. No error, no warning.

The rules of automatic semicolon insertion

The engine inserts a semicolon in exactly three situations:

  1. When the next token cannot continue the current statement and there is a line break before it. let a = 1\nlet b = 2 - let cannot follow 1, so a semicolon goes in.
  2. At the end of the file.
  3. After return, break, continue, throw, and after a variable or expression before ++ / --, if a line break comes first. These are the "restricted productions": the line break itself forces the semicolon.

What it does not do: insert a semicolon merely because a line ended. If the next line could continue the statement, the engine assumes it does. That is where the bugs come from.

The five cases that bite

1. A line starting with (

javascript

const total = a + b
(function () { console.log("init") })()
// read as:  const total = a + b(function () {...})()
// → TypeError: b is not a function

2. A line starting with [

javascript

const first = items
[1, 2, 3].forEach(log)
// read as:  const first = items[1, 2, 3].forEach(log)
// the comma operator picks 3 → items[3].forEach → probably TypeError

3. A line starting with a template literal

javascript

const name = user.name
`Hello ${name}`.trim()
// read as:  user.name`Hello…`  — a tagged template call on user.name

4. A line starting with + or −

javascript

let x = 5
-1
// read as:  let x = 5 - 1  → x is 4, not 5

5. return, throw, break with the value on the next line

The example at the top. The value must start on the same line as return. Open the brace or parenthesis on that line and it is fine.

An extra semicolon that is a bug

javascript

if (user.isAdmin);   // ← this semicolon ends the if
{
  showAdminPanel();     // runs for everyone
}

for (let i = 0; i < 3; i++);   // ← empty loop body
  console.log(i);              // ReferenceError: i is not defined

A semicolon directly after if (…), for (…) or while (…) is an empty statement. The condition or loop applies to nothing, and the block below runs unconditionally, once. Linters catch this (no-empty); the engine does not.

Where you never need one

  • After a block: if (x) { … }, for (…) { … }, function f() { … }. A closing brace ends the statement.
  • After a class body or a function declaration. (A function expression assigned to a variable does take one: const f = function () {};)
  • Inside a for (init; test; update) header - those are separators, not terminators, and there are exactly two.

So: semicolons or not?

Both styles are used by serious codebases. The standard library-style answer is "always write them" - then ASI never runs and none of the cases above apply. The "never" style (used by the JavaScript Standard Style, and popular in Vue and Svelte code) works as long as every line that starts with (, [, `, + or gets a leading semicolon: ;(function () {…})().

What matters more than the choice is not making it by hand. Run Prettier or ESLint's semi rule and let the tool enforce whichever you pick. The semicolon bugs above happen in code that is inconsistent, not in code that is consistently either way.

Common mistakes

Relying on ASI in minified or concatenated code

File A ends with x = 1 (no semicolon), file B starts with (function(){…})(). Concatenate them and you have case 1. Bundlers handle this, but a hand-rolled build script will not. Files should end with a semicolon or a newline - ideally both.

Assuming the linter has your back without configuring it

ESLint does not enforce semicolons by default. Turn on semi: ["error", "always"] (or "never") and no-unexpected-multiline, which flags the five cases regardless of style.

Putting the opening brace on its own line

Allman style - brace on the next line - is fine in C# and unsafe for return in JavaScript. That one construct is why JavaScript style guides universally put the brace on the same line.

Related chapters