The Infamous Floating-Point Math
One of the most well-known JavaScript jokes is typing 0.1 + 0.2 into the console and watching it produce 0.30000000000000004. But this quirk isn’t actually unique to JavaScript — it comes from the IEEE Standard for Floating-Point Arithmetic, which nearly every programming language follows when representing numbers.
Computers store numbers in binary within a fixed amount of memory. An 8-bit integer can hold values from 0 to 255, but only whole numbers. To represent decimals, we might use a fixed-point format, but that quickly sacrifices range. A fixed 8-bit representation, for instance, can only go from 0 to 15.9375.
Engineers working on a bridge can tolerate tiny measurement errors; someone manufacturing a microchip cannot. This is where floating-point arithmetic comes into play. It uses a sign bit, a significand containing the digits, and an exponent that tells the computer where to place the point — similar to scientific notation. That is why the point can “float” around.
An 8-bit floating-point format can handle values up to 480 and down to 0.0078, but not every number in that range can be represented. Computers use 32 or 64 bits to increase accuracy, but they still cannot cover all values. JavaScript numbers use 64-bit (double-precision) representation, which gives 53 bits for the significand. This means only integers in the range of -9007199254740991 to 9007199254740991 (the value of Number.MAX_SAFE_INTEGER) are safe from precision loss.
Why does the precision issue affect numbers well below that limit, like 0.1 and 0.2? Just as the decimal system cannot represent one-third without an infinitely repeating sequence, the binary system cannot represent some common decimals exactly. The number 0.2, when converted to binary, produces an infinitely repeating pattern of 1001 digits. Since the significand has a fixed size, those digits must be truncated.
0.001 1001 1001 1001 1001 1001 10 [...]
When you convert the stored double-precision value of 0.2 back to base-10, you do not get 0.2 — you get an approximation. For most operations, like 0.2 + 0.2, the imprecision is so small that JavaScript silently rounds the result. In some cases, however, the accumulated error escapes this rounding behavior:
0.1000000000000000055511151231257827021181583404541015625
If you were to add the actual stored values of 0.1 and 0.2 together, then round the result, you would get 0.30000000000000004. You can verify your system against this by inspecting the stored values on float.exposed.
0.3000000000000000444089209850062616169452667236328125
Floating-point arithmetic has well-documented quirks, but because it is standardized worldwide, one big consolation is consistency: every modern system yields the same result. The answer may be unexpected, but it is predictable.
Loose Equality and Type Coercion
JavaScript is both dynamic and weakly typed. It does not require you to declare what type a value holds, and it can silently convert between types — particularly when using the loose equality operator (==) or the plus sign (+). The conversion rules are convoluted enough that behavioral errors are quite easy to trigger.
When comparing a string with a number using ==, JavaScript will convert the string into a number:
console.log("2" == 2); // true
The plus operator reverses that rule. With +, JavaScript prefers string concatenation, coercing a number into a string when other strings are present:
console.log(2 + "2"); // "22"
For this reason, the plus sign is only safe with known numeric operands. For string work, curly braces template literals offer a much clearer and less surprising experience.
The origins of the convenience-based coercion are quite quirky even by JavaScript history standards. Brendan Eich has publicly recounted how early adopters asked for language tweaks, and he most notably had to deal with a request that led to the distinction between == and ===. A user wanted to compare numbers to strings without doing the manual conversion first, so Eich introduced the loose equality operator — essentially for that very specific convenience.
Contributing to this outcome, Guy L. Steele of Scheme and Lisp fame pointed out that having multiple equality operators was not an issue in Lisp — contexts where several dialects supported five distinct equality checks. That mentality opened the door for JavaScript’s dual operator approach. But unlike a Lisp dialect, JavaScript is a living standardization with huge backwards compatibility stakes.
Once a feature is in the language, it becomes effectively permanent, no matter how dangerous or error-prone it is. The loose equality operator will never be removed. The practical guidance is clear: hold to strict equality (===) and stay away from == and ambiguous type conversions entirely.
Automatic Semicolon Insertion (ASI)
JavaScript expects a semicolon at the end of several kinds of statements: declarations with let, const, or var, expression statements, return statements, break, continue, class field definitions, debugger, and do...while loops, plus import and export statements.
Still, actually writing the semicolons is now optional thanks to Automatic Semicolon Insertion (ASI), a mechanism that deduces where ending semicolons should go. It was originally designed to help newcomers who did not know where a semicolon is mandatory. In practice, this inference fails far more often than it should:
const a = 1
(1).toString()
const b = 1
[1, 2, 3].forEach(console.log)
Nothing wrong stands out here until you ask JavaScript to interpret it. With completely explicit semicolons, the code would resemble this correctly grouped sequence:
const a = 1;
(1).toString();
const b = 1;
[(1, 2, 3)].forEach(console.log);
Without them, however, the results are full of syntax and type errors. JavaScript will treat the statements as one single chaotic line:
const a = 1(1).toString();
const b = (1)[(1, 2, 3)].forEach(console.log);
ASI is not a feature to hang your hat on. Although many linters and formatters produce standardized semicolon placement, even they cannot save you from the subtle pitfalls that result in uncaught exceptions. Explicit semicolons remain the only safe route.
Two (or Three) Kinds of “Nothing”
JavaScript has more than one way to say “there’s nothing here,” and that redundancy is a known design wart. The terms null and undefined both represent bottom values — values that don’t exist or are undefined. Everything else in the language, including primitives, can be boxed into objects, but these two cannot. Accessing a property on either one throws an exception.
undefined is JavaScript’s default bottom value. Reading a variable that was declared but never initialized returns undefined, as does accessing a property that doesn’t exist on an object. To stay consistent with how the language behaves, prefer undefined in your own code whenever you need to represent a missing value on an existing property or variable.
null, by contrast, is meant to signal the absence of an object — which is why typeof null misleadingly returns "object". JavaScript itself uses null to mark the end of recursive structures like the prototype chain. In practice, you can often use undefined instead, but not always. Object.create(null) is the only way to create an object without a prototype; passing undefined there raises a TypeError.
Both null and undefined cause the same “path problem”: trying to read a nested property fails as soon as you hit a bottom value.
let user;
let userName = user.name; // Uncaught TypeError
let userNick = user.name.nick; // Uncaught TypeError
The typical workaround is either chaining with logical AND (&&) or using optional chaining (?).
let user;
let userName = user?.name;
let userNick = user && user.name && user.name.nick;
console.log(userName); // undefined
console.log(userNick); // undefined
Some also consider NaN a third bottom value — one that represents the absence of a number, often produced by a failed string-to-number conversion. It has its own quirk: it’s never equal to itself. Use Number.isNaN() to test whether a value is actually NaN.
You can test for all three bottom values in one expression:
function stringifyBottom(bottomValue) {
if (bottomValue === undefined) {
return "undefined";
}
if (bottomValue === null) {
return "null";
}
if (Number.isNaN(bottomValue)) {
return "NaN";
}
}
The Case Against ++ and --
Developers read code far more often than they write it, so readability usually beats brevity over time. That’s a strong argument against using the increment (++) and decrement (--) operators, which add special syntax just to add or subtract one.
The bigger issue is the pre-increment and post-increment forms. Where you place the operator changes the result, and it’s easy to mix them up — leading to subtle bugs that are painful to trace. The historical justification for these operators comes from C, where they were designed for pointer arithmetic: stepping forward or backward through memory addresses. We now know pointer arithmetic is dangerous and a common source of memory bugs, yet the syntax survived and made its way into JavaScript.
Writing + 1 or - 1 instead is clearer and avoids the entire class of confusion around operator placement. It’s not a critical change, but it’s an easy way to improve code clarity.
Understanding the Quirks
JavaScript’s most baffling features usually trace back to historical compromises and an impossible attempt to satisfy everyone. The language has no obligation to accommodate every developer — but developers do have an obligation to learn how the language actually works, to use its strengths and to stay alert to its quirks.
Many of these decisions become more understandable with history in mind. The prototypal inheritance model was obscured in the beginning, and the this keyword remains a multipurpose source of confusion. But none of that excuses skipping the deep dive. The more you understand about the language’s origins, the less “senseless” it becomes.



