Master JavaScript

What JavaScript is, and how to run it

6 / 266
Contents

Contents

Master JavaScript

0 of 266 complete

10 min read

Semicolons you did not write

JavaScript finishes some statements for you, and the two line shapes where it guesses wrong break your program quietly.

Ask two JavaScript programmers whether to end lines with a semicolon and you will get an argument. The argument is usually about taste. Underneath it is a rule in the grammar that both camps have to know, because it is the rule that decides what your program means.

Start with the reason the argument exists at all.

Try CodeJavaScript

Put a semicolon at the end of each of the three lines and run it again. Nothing changes.

Not one semicolon, and it runs. The grammar of the language says a declaration ends with a semicolon, so where did they come from? The parser put them in.

This is called automatic semicolon insertion, usually shortened to ASI. It is part of the specification, not a kindness from one engine, so it happens everywhere and has done since 1997.

The dangerous part is the exact rule, which is not the one most people carry in their heads. Most people believe a line break ends a statement. It does not. Here is the counterexample, and it is worth running before reading the explanation.

Try CodeJavaScript

It stops with TypeError: b is not a function. Put a semicolon at the end of the const sum line and run it again.

Nothing on the failing line mentions calling b. To see where the call came from, take the line break out and read what is left: const sum = a + b(a + b).toString(). The parser did exactly that.

It reached the end of const sum = a + b, looked at the next token, and found (. A ( can perfectly well continue that statement, because b(...) is a call. Since carrying on was possible, no semicolon was inserted, and b is the number 2 rather than a function.

The rule, stated

A semicolon is inserted in three situations, and in no others.

  1. The parser meets a token it cannot use to continue the current statement, and there is a line break before that token. It inserts a semicolon in front of it.
  2. The parser reaches the end of the file with a statement still unfinished. It inserts one at the end.
  3. The parser meets a } that would otherwise be a syntax error. It inserts one before the brace.

Read rule one again and notice what it is not. It does not say "a line break ends a statement". It says a semicolon appears only when carrying on would be a syntax error. If the next line can be read as a continuation, it is read as one, however plainly you meant otherwise.

The other shape: a keyword left dangling

There is a second rule sitting on top of the first, and it goes the opposite way: a handful of keywords may not have a line break after them, and if one does, a semicolon is forced in whether it helps or not.

Seeing it needs one construction from a later section. A function is a named piece of code that you can call, and return is how it hands a value back. Functions get a section of their own; here you only need those two sentences.

Try CodeJavaScript

Move 2 + 3 up onto the same line as return and run it again.

No error, no warning, and the answer is undefined rather than 5. A semicolon was forced in straight after return, so the function returns nothing, and 2 + 3 sits below as a line that computes five and throws it away.

The keywords with this restriction are return, throw, break, continue and yield, plus ++ and -- when written after the thing they change. The specification calls these restricted productions, meaning a line break in that one position is not allowed to be ignored.

Try CodeJavaScript

It prints 1 3. Rewrite the middle two lines as a++ and then b, and predict the two numbers before you run it.

Reading down the page, a then ++b looks like it might be a++ followed by b. It is not. ++ cannot have a line break before it when it comes after its target, so the parser is forced to end the statement after a, and ++b becomes an instruction of its own that raises b to 3.

One of those keywords is loud about it

The forced semicolon after return is silent because what it leaves behind is legal: return; is a complete statement meaning "give back nothing". throw is the exception, and the difference is worth a minute.

throw is how a program raises an error deliberately, and it must be given something to raise. The section on errors covers it properly; here it is only the shape that matters.

function fail() {
  throw
  new Error("boom");
}

// SyntaxError: Illegal newline after throw
Not runnable. The file is rejected while it is being read.

The rule that fired is exactly the one that fires after return. The outcome differs because throw; is not a statement the grammar allows, so the insertion leaves something meaningless and the parser says so, by name, before anything runs.

So of the restricted keywords, throw is the safe one: put the line break in the wrong place and you are told. return, break and continue all leave behind something legal, and legal is another word for silent.

The rule that inserts a semicolon anyway

There is a fourth situation, added to the specification in 2015, and it goes against everything above: a semicolon is inserted even though carrying on would have been possible.

It exists for one construction. A loop repeats a piece of code; the do ... while form runs the code once and then keeps repeating for as long as its condition holds. Loops have a section of their own, so read the next snippet for its ending rather than its counting.

Try CodeJavaScript

It prints counted to 3. Now replace the do ... while line with const n = i, and run it again: with any other statement above it, the third line is joined on as a call, and it stops with TypeError: i is not a function.

The third line starts with (, which by rule one should have joined it to the line above, giving while (i < 3)(...). It did not, because the specification says a semicolon is inserted after the closing bracket of a do ... while whatever comes next. One statement in the language is protected from the trap by name.

A line that begins with a brace

The previous lesson said that { } is a block in statement position and an object in expression position, and that the difference costs something. This is the cost.

Try CodeJavaScript

Both messages appear and nothing complains. Now add a line at the end reading console.log(theme); and read the error: nothing called theme was ever created.

The first { theme: "dark" } follows an =, which wants a value, so it is an object. The second one starts a line, where the grammar wants a statement, so it is a block.

Inside that block, theme: is read as a label: a name attached to a statement, which exists so that break can name which loop to leave. The "light" after it is an expression statement, evaluated and thrown away. Nothing was created, nothing was stored, and nothing was said about it.

Add a second property and the disguise fails, because two labels in a row is not a shape the grammar has.

{ a: 1, b: 2 }

// SyntaxError: Unexpected token ':'
Not runnable. Written at the start of a line, with nothing in front of it.

The parser took a: as a label, then read 1, b as a single expression, and then met a colon it had no use for. The caret in the message points at the colon after b, which is where it gave up rather than where the misunderstanding began.

This is where the well-travelled puzzle comes from. At the start of a line, {} + [] is an empty block followed by +[], and the answer is the number 0. Inside brackets there is no statement position, so ({} + []) is an empty object added to an empty list, and the answer is the text "[object Object]". The two lines look like the same program and are not.

The practical rule is short. If a line has to start with an object, or an object has to be handed straight to something, put brackets round it: ({ a: 1 }). That puts it in expression position, where braces mean what you wanted them to mean.

The characters to watch

Every ASI accident of the first kind starts a line with one of six characters: (, [, a backtick, +, - and /. If a line begins with any of them, the parser will try to join it to the line above.

First characterRead as
(A call of whatever ended the previous line
[An index into whatever ended the previous line
A backtickA tagged template attached to the previous line
+ or -Addition or subtraction continuing the previous line
/Division continuing the previous line
What the parser sees when a line starts with each character. Five rows, six characters, because `+` and `-` behave identically here.

What to actually do

Two policies work, and they are the two you will see in real projects. Both are fine. What is not fine is following neither, which is where the accidents live.

  • Write them. End every statement with a semicolon. The trap of the first kind then cannot happen at all, and only the return shape is left to remember.
  • Leave them out. Then put a semicolon at the start of any line beginning with one of the six characters above, so ;(a + b).toString(). This is why you sometimes see a line that opens with a semicolon in code that otherwise has none.
You write const items = fetchAll() on one line, and on the next [0].name. Is a semicolon inserted between them?

No. [ can continue the statement, as an index, so the parser reads fetchAll()[0].name and assigns that to items. If you meant two statements you have to say so, either with a semicolon at the end of the first line or one at the start of the second.

Check yourself

In a function body, throw is alone on one line and new Error("boom"); is on the next. Put return in that position and the function quietly gives back undefined. What happens with throw?

All lessons in Master JavaScript