With some of the big changes to the frontend tooling landscape over the past two years, I’ve been reconsidering not just the way that I build frontends but also the way I write code.
One of the things I’ve been thinking about lately has been something I haven’t thought about in years: semicolons.
I should point out, however, that whatever your opinion of them in JavaScript, you’re probably right. I think it’s more important to be consistent with your choice than to go back and forth between semicolons and no semicolons.
That being said, do we really need semicolons in JavaScript?
We Have the Technology
I think for me, the answer to that in our modern frontend landscape is now a no.
Virtually all new frontend projects begin with a sophisticated framework toolchain that is supplied by a CLI or an npm initializer. They all have options or tools that take care of linting, formatting, transpiling, and compiling for us.
We have all of this infrastructure set up just to make our development experience better and keep code consistent across teams. With all of these tools at our disposal I think we can afford to ditch the semicolons in JavaScript for cleaner-looking code.
Why do it? There is no right answer. It’s a personal choice.
Why Semicolons in JavaScript (Kind Of) Suck
To be clear, semicolons don’t suck. JavaScript’s implementation of semicolon insertion does. The language tells you that they are technically “optional” but it fumbles the implementation and ends up creating a few edge cases where not using semicolons will get you in trouble.
I think the ESLint documentation explains it very well so I will quote them for illustration:
JavaScript doesn’t require semicolons at the end of each statement. In many cases, the JavaScript engine can determine that a semicolon should be in a certain spot and will automatically add it. This feature is known as automatic semicolon insertion (ASI) and is considered one of the more controversial features of JavaScript. For example, the following lines are both valid:
var name = "ESLint"
var website = "eslint.org";
On the first line, the JavaScript engine will automatically insert a semicolon, so this is not considered a syntax error. The JavaScript engine still knows how to interpret the line and knows that the line end indicates the end of the statement.
This problem leads to two edge cases which will create trouble for those not using semicolons:
- Unreachabe code
- Unexpected multiline code
Unreachable Code
However, the ASI mechanism can sometimes be tricky to people who are using semicolons. For example, consider this code:
return
{
name: "ESLint"
};
This may look like a return
statement that returns an object literal, however, the JavaScript engine will interpret this code as:
return;
{
name: "ESLint";
}
Effectively, a semicolon is inserted after the return
statement, causing the code below it (a labeled literal inside a block) to be unreachable.
Unexpected Multiline Code
On the other side of the argument are those who say that since semicolons are inserted automatically, they are optional and do not need to be inserted manually. However, the ASI mechanism can also be tricky to people who don’t use semicolons. For example, consider this code:
var globalCounter = { }
(function () {
var n = 0
globalCounter.increment = function () {
return ++n
}
})()
In this example, a semicolon will not be inserted after the first line, causing a run-time error (because an empty object is called as if it’s a function).
The Rules
The ASI rules are straightforward, but easy to miss in a language with so much going on like JavaScript:
The rules for ASI are relatively straightforward: As once described by Isaac Schlueter, a newline character always ends a statement, just like a semicolon, except where one of the following is true:
- The statement has an unclosed paren, array literal, or object literal or ends in some other way that is not a valid way to end a statement. (For instance, ending with
.
or,
.) - The line is
--
or++
(in which case it will decrement/increment the next token.) - It is a
for()
,while()
,do
,if()
, orelse
, and there is no{
- The next line starts with
[
,(
,+
,*
,/
,-
,,
,.
, or some other binary operator that can only be found between two tokens in a single expression.
In the exceptions where a newline does not end a statement, a typing mistake to omit a semicolon causes two unrelated consecutive lines to be interpreted as one expression. Especially for a coding style without semicolons, readers might overlook the mistake. Although syntactically correct, the code might throw exceptions when it is executed.
Using the Tools
Now that we’ve seen how semicolons work in JavaScript, we can explore ways to have our tools work for us and prevent edge cases where not using semicolons would lead to runtime errors.
There are three widely used and adopted tools in the JavaScript and TypeScript ecosystem today that we can use as a strong line of defense for not using semicolons:
- ESLint
- Prettier
- TypeScript Compiler (TSC)
These three work quite well together, each providing its own protection against the edge cases described previously. They are usually integrated with an IDE like VSCode to make them “just work” and provide a solid code editing experience.
The nicest experience is to have auto-linting, auto-formatting, and auto-type-checking set up for these three tools in our IDE and workflow.
ESLint
ESLint is the most popular JavaScript linter. It is highly configurable and comes with recommended rules out of the box.
To configure ESLint to work without the use of semicolons and properly lint our edge cases, we just need to specify these three rules: semi, no-unreachable, and no-unexpected-multiline.
To do so, we can add the following code snippet to our .eslintrc.{js,yml,json}
config file under the rule
block:
rules: {
semi: ['error', 'never'],
'no-unreachable': ['error'],
'no-unexpected-multiline': ['error']
}
That’s it! It’s simple. Now ESLint will enforce the use of no semicolons and will warn us when we might be in trouble.
Note that ESLint already turns on the no-unreachable
and no-unexpected-multiline
rules out of the box when we extend our config file with the eslint:recommended configuration, so we might not need to add these two rules at all.
Prettier
Prettier is an opinionated code formatter with one job and one job only: to format our code.
You will usually see Prettier being used right alongside ESLint with some plugins to make them play nice with each other.
We can configure Prettier to automatically remove semicolons from our code just by adding one line to our .prettierrc.json
config file, the semi option:
{
"semi": false
}
Prettier is even smart enough to introduce semicolons at the beginning of lines that may lead to ASI failures.
We can also format all of the files in our project to remove semicolons with the npx prettier --write .
command.
Note that using Prettier together with ESLint usually involves the use of the eslint-config-prettier package. This package turns ESLint rules off based on our Prettier config file. If this is the case, no changes to our ESLint config file are needed to support the "semi": false
option as long as the ESLint config is also extending the eslint:recommended
configuration.
TSC
The TypeScript compiler is the simplest of all three, as it requires no configuration for using the language without semicolons. It does require us to use TypeScript, which might not be the case for many frontend projects. In that case, the coverage offered by the previous two tools should be enough.
Although it does not cover every edge case, the compiler will emit errors when some of these ASI failures lead to an invalid use of TypeScript’s type system. You can read more about these scenarios in this article.
When to (Still) Use Semicolons
You should consider the use of semicolons when:
- Writing code in a project or environment without good tooling support
- You’re new to JavaScript
- Because you just want to
Basically any scenario where developers > 1
and are on their own when it comes to ASI is a scenario where you don’t want to introduce that risk.
If you have to think about it… it’s a problem or you might have a bikeshed on your hands.