DEV Community

Cover image for JavaScript Type Coercion Bugs That Will Break Your App
Bikki Singh
Bikki Singh

Posted on • Originally published at codepractice.in

JavaScript Type Coercion Bugs That Will Break Your App

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!
}
Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

=== 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode
// GOOD — convert explicitly before doing arithmetic
let age = "25";
let nextYearAge = Number(age) + 1;
console.log(nextYearAge); // 26
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
// 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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// GOOD — check length, not truthiness, for arrays
if (cart.length > 0) {
  console.log("Cart has items");
} else {
  console.log("Cart is empty");
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
// GOOD — use Number.isNaN() to check for NaN
if (Number.isNaN(result)) {
  console.log("Invalid number"); // correctly runs
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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.stringify around localStorage
  • .length instead 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:

Official references:

Originally published at codepractice.in

Top comments (0)