This is a first of a series I thought I’d start writing (and hopefully continue writing) about my experiences over 36 years as a programmer, and 20+ years as a professional web developer. (Am I old now? I think I’m old.)
First, what are we talking about? If you’re new to JavaScript, you may know that JavaScript is rather forgiving of statement terminations. JavaScript isn’t alone in this. Go, Swift, Kotlin, Ruby, Python, Lua… they all make semicolons optional at the end of statements.
In JavaScript, the spec calls this Automatic Semicolon Insertion or ASI. The simplified explanation is that if JS sees what would be a syntax error, and it thinks a semicolon might fix it, it inserts one automatically.
Here’s an example:
let one = 1
let two = 2
Early in the culture of JavaScript development, almost all code examples you would find online, though, always had semicolon terminators, like:
let one = 1;
let two = 2;
The semicolon-less era
In the early 2010s, this started to shift. Web development, as a culture, was getting more and more sophisticated, which led to more subject-matter experts with in-depth knowledge, including of esoteric concepts like ASI.
Many JavaScript developers began to say, „Look, semicolons are optional 99% of the time. Why not just omit them, as a standard?“ For many, because semicolons seemed unnecessary, then they were just a form of visual noise.
Around the same time, teams were promoting coding with the same style, for easier maintenance and for making fewer decisions. So this new „standard“ began to be incorporated in linting tools so that semicolon terminators were now not just optional, they were actually forbidden in a given codebase.
Anyway, this is a lot of probably unnecessary history, which many readers may already know. The point is, why did I, after adopting semicolon-less JavaScript for over ten years, decide to abandon this as of this year (2025)?
99% is not 100%
JavaScript doesn’t require semicolons most of the time, but when it does, errors can suddenly become very unintuitive. Let’s take this code example (demonstrating variable swapping via array destructuring), which I borrowed from this Medium post:
let varOne = 'Hello'
let varTwo = 'World'
[varOne, varTwo] = [varTwo, varOne]
JavaScript will actually parse that second statement as:
let varTwo = 'World'[varOne, varTwo] = [varTwo, varOne]
If I paste the above code example in VSCode, and let TypeScript try to explain the error to me, I don’t get something like „semicolon missing between line two…“ blah blah blah, instead I get these errors:
'varTwo' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer
Type 'any[]' is not assignable to type 'string'
Index signature in type 'String' only permits reading.
Left side of comma operator is unused and has no side effects.
Sure, if you know ASI rules, after a bit of head-scratching, you may eventually realize a semicolon is necessary here. The reason is that JavaScript will not insert a semicolon before (
or [
at the start of a line,
You can, of course, just add in one of two ways: either the end of the second statement or the start of the third, like:
// Option 1
let varOne = 'Hello'
let varTwo = 'World';
[varOne, varTwo] = [varTwo, varOne]
// Option 2
let varOne = 'Hello'
let varTwo = 'World'
;[varOne, varTwo] = [varTwo, varOne]
Interestingly most „Ditch your Semicolon“ advocates have recommended Option 2, because you’re not altering the second statement in order to resolve the third.
Code can start to look weird fast
Despite many advocates insisting that the above example was rare, as I said, it can make errors unintuitive, but, also, if you’re working in a team where someone needs to review your code and isn’t as familiar with ASI quirks, the above solutions can easily look like a typo.
Additionally, in the 2010s, destructuring patterns, including re-assignment via destructuring, were not as prevalent, as it only began to be supported in middle of 2016.
For these reasons, over time, I found myself needing to pepper in semicolons, because:
- I began to use destructuring more in my own code, including re-assignment via destructuring.
- I began to use TypeScript almost exclusively.
TypeScript: adding additional problems for ASI
Why would TypeScript make this issue worse? Let’s take the following code, in JS:
let input = document.querySelector('input.url')
input.focus()
This works fine! But let’s take this into TypeScript:
let input = document.querySelector('input.url')
input.focus()
// - errors: 'input' is possibly null
// Property 'focus' does not exist on type 'Element'
Okay, let’s fix that by casting this to what we know this element actually is.
let input = document.querySelector('input.url')
(input as HTMLInputElement).focus()
All of the sudden, both these lines are reporting errors, even though we only changed the second line. Why? Because TypeScript imports all the same ASI quirks as JavaScript, meaning a line that starts with (
continues the statement.
This began to increasingly happen to me. I would fix a type problem, which would then require me to wrap something in parentheses, which then would create new errors, which then I’d grumble and insert an awkward-looking semicolon there at the start of the statement.
What was the issue with semicolons in the first place?
Eventually, I began to wonder what I was really gaining by leaning on ASI. As software developers, we’re used to looking at semicolons at the end of statements. In fact, if you’re a web developer, you’re used to putting them at the end of CSS declarations. They just aren’t a big deal.
And as to visual noise? Because they are so ubiquitous, they start to visually disappear anyway.
They don’t even arguably require more typing. I have both ESLint and „Fix on save“ enabled in VSCode, meaning every save will just quietly fix any missing semicolons.
As a result, I’ve discovered that I actually think about semicolons less by using them than by not using them. There’s just less cognitive overhead. Statements end with semicolons, full stop. Same-line statements are separated with semicolons. (Almost) all statements have semicolon companions. I no longer have to think about semicolons while destructuring or fixing type problems. My code is no longer confusing my C# colleagues who may be reviewing it.
Moving forward;
Anyway, for me, while this is arguably not the largest issue or problem to have, I thought it would be an interesting dive into code style decisions. And I would imagine some of the same logic may apply to other code that uses optional semicolons, and I’d be interested to hear about that!
Photo by Sudharshan TK on Unsplash