You check a form field, everything looks fine in the console, and then production tells you role was 0 and your if (role == false) check just let an unauthorized user through.
Nobody touched that code in weeks. Nothing "broke." JavaScript just did exactly what it was designed to do — quietly convert types behind your back.
A type coercion bug happens when JavaScript's automatic type conversion kicks in somewhere you weren't expecting it — usually inside a comparison (==), an arithmetic operation (+, -), or a truthiness check (if, &&, ||). The fix is almost always the same: stop letting JavaScript guess, and tell it exactly what type you mean.
What is type coercion, exactly?
Type coercion is the automatic conversion of a value from one data type to another that JavaScript performs behind the scenes when an operator is used on mismatched types — for example, converting the number 1 to the string "1" when you write 1 + "2".
Under the hood, this is governed by the ECMAScript spec's internal conversion algorithms — ToPrimitive, ToNumber, ToString, and the Abstract Equality Comparison used by ==. Think of it like a friend who "helps" by translating everything into whatever type feels convenient at the moment. Sometimes useful, sometimes it quietly changes what you meant.
If you're still shaky on how JavaScript variables hold their values in the first place, it's worth revisiting Var vs Let vs Const in JavaScript before going further — scoping mistakes often compound with coercion bugs.
Why this keeps breaking production code
JavaScript is loosely typed — a variable's type isn't locked in, and operators convert operands rather than throw an error when types don't match. Convenient when prototyping. A liability when you're shipping code other people depend on.
The bug that gets most developers here is assuming == and === behave the same "most of the time." They don't. == runs a multi-step coercion algorithm before comparing, and that algorithm has edge cases most people can't recite from memory. Code that "just works" in dev because your test data happened to dodge the edge case breaks the moment real user input hits it.
A real production example
I've seen this break in production when a permissions system checked role against false instead of checking for a specific value:
// BAD — role 0 is a valid ID, but this check treats it as "false"
let role = 0; // user's actual role ID from the database
if (role == false) {
console.log("No role assigned"); // this runs — but role IS 0, a real role!
}
false coerces to 0 during the == comparison, and 0 == 0 is true. If 0 is a legitimate role ID in your system, this incorrectly treats a real user as having no role.
// GOOD — strict equality, no coercion, checks exactly what you mean
let role = 0;
if (role === false) {
console.log("No role assigned"); // does NOT run — 0 !== false, correct
}
// Even better — check explicitly for what "no role" actually means
if (role === null || role === undefined) {
console.log("No role assigned");
}
=== never coerces. If the types don't match, it returns false immediately, so a number is never mistaken for a boolean.
Implicit vs explicit coercion
Implicit coercion happens automatically, as a side effect of an operator — you didn't ask for it, JavaScript just did it.
console.log("5" + 1); // "51" — number 1 becomes a string
console.log("5" - 1); // 4 — string "5" becomes a number
console.log(true + 1); // 2 — true becomes 1
Explicit coercion is you doing it on purpose, with Number(), String(), or Boolean().
let userInput = "42";
let asNumber = Number(userInput); // 42, a real number
let asString = String(100); // "100", a real string
let asBool = Boolean(userInput); // true, non-empty string
The rule that saves you the most pain: if you find yourself typing Number(...) or String(...), you already know what you're asking for. That's the whole point of explicit conversion — no surprises.
Coercion bugs you'll actually hit
1. + vs - behaving differently
+ does string concatenation the moment either operand is a string. Subtraction, multiplication, and division always convert to numbers — only + has this dual behavior.
// BAD — silent string concatenation instead of math
let age = "25"; // form input values are ALWAYS strings
let nextYearAge = age + 1;
console.log(nextYearAge); // "251" — not 26!
// GOOD — convert explicitly before doing arithmetic
let age = "25";
let nextYearAge = Number(age) + 1;
console.log(nextYearAge); // 26
This is one of the most common form-input bugs in production — document.getElementById('age').value always returns a string, even for a type="number" input.
2. Trusting localStorage to remember types
localStorage and sessionStorage only store strings. Save a boolean, read it back, and you get a string — which is truthy no matter what it says.
// BAD
localStorage.setItem("isDarkMode", false);
let isDarkMode = localStorage.getItem("isDarkMode");
if (isDarkMode) {
console.log("Dark mode is on"); // runs! "false" is a non-empty string = truthy
}
// GOOD — convert back to a real boolean explicitly
localStorage.setItem("isDarkMode", JSON.stringify(false));
let isDarkMode = JSON.parse(localStorage.getItem("isDarkMode"));
if (isDarkMode) {
console.log("Dark mode is on"); // correctly does NOT run
}
3. Assuming empty arrays are falsy
Only eight values are falsy in JavaScript: false, 0, -0, 0n, "", null, undefined, and NaN. An empty array is not one of them.
let cart = [];
if (cart) {
console.log("Cart exists"); // runs — [] is truthy, even when empty!
}
console.log(Boolean([])); // true
// GOOD — check length, not truthiness, for arrays
if (cart.length > 0) {
console.log("Cart has items");
} else {
console.log("Cart is empty");
}
4. NaN never equals itself
NaN is the only value in JavaScript that isn't equal to itself — not with ==, not with ===.
let result = "abc" * 2; // NaN
if (result == NaN) {
console.log("Invalid number"); // never runs, NaN == NaN is always false
}
// GOOD — use Number.isNaN() to check for NaN
if (Number.isNaN(result)) {
console.log("Invalid number"); // correctly runs
}
Per the MDN docs, Number.isNaN() is the reliable way to check this, since the global isNaN() coerces its argument first and can give false positives on non-numeric values.
5. JSON.stringify silently drops values
JSON.stringify coerces values silently — undefined, functions, and symbols are dropped from objects entirely, not converted to strings. Inside arrays, they become null instead.
let user = {
name: "Vicky",
role: undefined,
greet: function () {},
};
console.log(JSON.stringify(user)); // {"name":"Vicky"} — role and greet vanished!
Assuming JSON.stringify preserves every property is a bug that ships to production constantly, especially when sending API payloads. This isn't unique to JavaScript either — see The Same Array Bug in PHP, Python & JavaScript for how the same root cause shows up across languages.
== vs ===: when to use which
| Feature |
== (Loose Equality) |
=== (Strict Equality) |
|---|---|---|
| Type conversion | Converts operands before comparing | No conversion — types must match |
| Predictability | Low — depends on coercion rules | High — same type, same behavior always |
| Recommended default | No | Yes |
| Valid use case |
x == null (catches null and undefined in one check) |
Everything else |
Default to === everywhere. The one accepted exception is the x == null idiom, which intentionally catches both null and undefined in a single comparison.
Wrapping up
Every coercion bug in this post comes from the same root cause: letting the engine guess a type instead of telling it explicitly.
-
===over== -
Number()on form input -
JSON.parse/JSON.stringifyaround localStorage -
.lengthinstead of array truthiness -
Number.isNaN()instead of== NaN
Five habits, and most of these bugs stop happening. Next time you write a comparison or do arithmetic on a value that might not be the type you assume, pause for one second and convert it explicitly. That one second is cheaper than the 2am debugging session.
Further reading:
- Var vs Let vs Const in JavaScript – Key Differences, Example
- The Same Array Bug in PHP, Python & JavaScript — Explained
Official references:
Originally published at codepractice.in
Top comments (0)