- Home
- /
- Tutorials
- /
- JavaScript Tutorial
- /
- JavaScript Semicolons
JavaScript Statement & Expression
JavaScript Semicolons
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:
- When the next token cannot continue the current statement and there is a line break before it.
let a = 1\nlet b = 2-letcannot follow1, so a semicolon goes in. - At the end of the file.
- 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 function2. 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 TypeError3. 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.name4. A line starting with + or −
javascript
let x = 5
-1
// read as: let x = 5 - 1 → x is 4, not 55. 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 definedA 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
- JavaScript statements
- JavaScript syntax
- The return statement
- IIFEs - where the leading-semicolon habit comes from
