The + Operator as a Silent Carrier of Bugs
In JavaScript, there’s another surprising operation that might seem mathematical, logical, and consistent to us - I’m talking about the +
Here’s our favorite!
| |
JavaScript was meant to be a simple and fast language for web designers at the time. But has freeing programmers from static typing always had its advantages?
Let’s go back 28 years to the early versions of JavaScript implemented in the SpiderMonkey engine, specifically JavaScript 1.3, which was included in Netscape Navigator 4.06 in 1998.
| |
We will focus on an interpreter that supports this behavior.
The entire JSOP_ADD is about 60 lines long, but the decision between “number or string” is made in just five lines.
The rest involves protecting values from the garbage collector and manually concatenating a character buffer:
| |
| |
The logic itself is easy to understand, both when applying it to our input data and when analyzing the conditional statements.
| |
If lval or rval is a string, the entire expression is treated as a string; otherwise, it is converted to a number, that is:
| |
Looking at this code from 28 years ago, one might wonder: Was it a bug, as in the case of typeof null === "object", or a feature?
Just a quick glance at this code is enough - someone who checks lval and rval and then decides whether they are numbers or strings isn’t doing so by accident.
To make it more fun, let’s try some other math problems
| |
So why do -, *, and / work fine?
Because in this file, a few dozen lines down, subtraction and multiplication are generated by a single macro, without any concatenation logic:
| |
+ as an arithmetic operation and string concatenation
We already know that the operator + performs two functions: arithmetic operations and string concatenation.
So let’s take a look at how the neighbour from the same camp does it - Java ;-)
| |
| |
| |
The same error! - Does this mean that a statically typed language can’t prevent this error?
Let’s modify our program:
| |
| |
| |
And that’s the whole difference! - It’s a great idea, but… in Java, the bug is reproducible only when the result doesn’t end up in a context that requires a number.
So basically, only if you write it out right away or append it to another string.
It stops being passed the moment you want to do anything with that value - assign it to int, pass it to a method, store it in an entity field, and so on.
Static typing does not eliminate the mistake. It limits its scope to a single line and turns a silently wrong value into a loud refusal to run.
Why hasn’t this been fixed?
You could say it was a deliberate compromise rather than an oversight. The code we were looking at is from 1998, but the rule itself is three years older. In 1995, there were no developer tools or debuggers. The website’s author had no way to quickly fix such an error, and the page still rendered and was clickable anyway.
With twenty lines of code to change the image on hover, it was a good deal. With two hundred thousand lines of code handling payments, it’s terrible.
The cost is a specific combination of three decisions at once: the same operator handles both concatenation and addition, the result is determined only at runtime, and the direction of the conversion depends on the operator - + pulls toward a string, while - pulls toward a number.
TypeScript - promises it will be a number!
TypeScript - you could call it a “guardian” of JavaScript, but does it solve the problem?
| |
We make a beautiful promise - unfortunately, only at compile time.
But what happens at runtime?
We don’t know, because JSON.parse returns any, so TypeScript no longer knows anything about this value beyond our declaration.
| |
It depends on what gets sent ;-)
Undoubtedly, this is still a big help because it ensures consistency within your code - no one will accidentally pass a string there.
But as doesn’t generate any output code: after compilation, all that remains is a plain JSON.parse.
This isn’t a check - it’s just a promise made to the compiler - and no one verifies it at runtime.
Validation at the boundary
How can you protect yourself from this?
Validate at the boundary of your application, so you can be sure that what you expect matches what you actually receive.
In the following example, we will use zod.
| |
| |
What the compiler didn’t catch, zod validates at runtime :)
Summary
The + rule has been around for over 30 years and is untouchable due to backward compatibility.
What we can do is limit its scope by using tools such as:
@typescript-eslint/restrict-plus-operands- an ESLint rule that catches mixed+- TypeScript - but only within your own code, not at the system boundary
- schema validation (zod, valibot, arktype)
Number(x)andNumber.isFinite
And just how strong this legacy is can be seen in BigInt, which was added to the language in 2020.
| |
The first line throws an error because no one has written that combination before - there’s nothing to break. The second one still concatenates because the concatenation rule is a quarter of a century older.
New types throw an error instead of being silently converted. But when there’s a conflict with a rule that’s been around since the beginning of JavaScript, the old rule wins hands down!



