Senior 6 min · March 05, 2026

JavaScript Type Coercion — Empty Inputs Cause $0.00 Bug

Empty input fields caused + to concatenate strings, producing NaN that displayed as $0.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Type coercion is JavaScript converting a value from one type to another when an operation requires it
  • The + operator favours strings (concatenation); -, *, /, % favour numbers (arithmetic)
  • == coerces before comparing; === never coerces — use === everywhere by default
  • Only six values are falsy: 0, '', null, undefined, NaN, false — everything else is truthy
  • Form inputs always return strings — Number(inputValue) before arithmetic or you'll concatenate
  • Use ?? instead of || for defaults when 0, false, or '' are valid values
Plain-English First

Imagine you're at a coffee shop and you hand the cashier a €20 note to pay a $15 bill. The cashier doesn't reject you — they just quietly convert your euros to dollars and give you change. That's type coercion: JavaScript sees two different types and quietly converts one to match the other so the operation can proceed. Sometimes that's genuinely helpful. Sometimes it gives you completely wrong change and you don't notice until a customer calls to complain.

Type coercion is responsible for some of the most baffling bugs in JavaScript — the kind where you stare at your screen thinking 'this should work' while the console laughs at you. It's also the single biggest reason JavaScript has a reputation for being 'weird' at dinner parties and on programming subreddits. But here's the thing: coercion isn't random. The rules are consistent, they're documented in the ECMAScript specification, and they're completely learnable. Understanding them is the difference between writing code that works by accident and code that works by design.

The problem coercion solves is straightforward. JavaScript is a dynamically typed language — variables don't have fixed types, and the engine doesn't stop you from mixing them. When you write '5' + 3, JavaScript has to make a decision: crash, or figure out what you probably meant. Coercion is JavaScript's attempt to be helpful. The trouble is that 'helpful' and 'correct' aren't always the same thing, and the rules JavaScript applies aren't always the ones you'd expect.

I've seen coercion bugs take down checkout flows, corrupt analytics pipelines, and quietly corrupt data for weeks before anyone noticed the numbers looked wrong. None of those bugs were exotic — they were all predictable if you knew the rules. That's what this article gives you. By the end you'll be able to predict exactly what JavaScript will do when it encounters mismatched types in comparisons, arithmetic, and logical expressions. You'll understand the difference between implicit coercion (JavaScript decides) and explicit coercion (you decide). And you'll know exactly which patterns to avoid and which to use with confidence — the kind of knowledge that makes you dangerous in a code review and genuinely useful when a junior asks why their numbers look wrong.

Implicit Coercion: When JavaScript Makes Decisions For You

Implicit coercion happens automatically, without you writing any conversion code. JavaScript's engine looks at the operator you're using and the types of the values involved, then quietly converts one or both to make the operation work. The key word is quietly — there's no warning, no error, no indication in the output that a conversion happened. Just a result that might be completely unexpected if you didn't know to look for it.

The two most important rules to internalise are: the + operator prefers strings — if either operand is a string, it converts the other to string and concatenates — and every other arithmetic operator (-,*,/,%) prefers numbers, coercing operands to number before operating. This asymmetry between + and every other operator is not a quirk or an accident. It was a deliberate design decision. It's also the root cause of probably seventy percent of the coercion bugs I've seen in production codebases.

Logical contexts — if statements, while loops, ternary operators, short-circuit evaluation — trigger a third flavour called boolean coercion. JavaScript converts the value to either truthy or falsy. Six values are falsy: 0, '', null, undefined, NaN, and false. Everything else is truthy — including empty arrays and empty objects, which catches people off guard constantly. An empty cart [] is truthy. An empty response object {} is truthy. If you're checking whether an array has items, check its length, not the array itself.

implicitCoercion.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ─── The + operator: string wins ───────────────────────────────────────────
const userAge = 25;
const ageLabel = 'You are ' + userAge + ' years old';
console.log(ageLabel);        // 'You are 25 years old' — number coerced to string
console.log(typeof ageLabel); // 'string'

// The classic form input trap — priceFromInput looks numeric, but it's a string
const priceFromInput = '49'; // value from an HTML text input — always a string
const taxRate = 5;
const totalWrong = priceFromInput + taxRate; // '495' — NOT 54!
console.log('Wrong total:', totalWrong); // '495' — string concatenation, not addition

// Subtraction coerces to number — this is why the bug can hide for a while
const totalRight = priceFromInput - taxRate; // '49' coerced to 49, then 49 - 5
console.log('Right total:', totalRight); // 44 — correct

const multiplied = priceFromInput * 2;
console.log('Multiplied:', multiplied); // 98 — '49' coerced to number

// ─── Boolean coercion: the six falsy values ─────────────────────────────────
const falseValues = [0, '', null, undefined, NaN, false];
falseValues.forEach((val) => {
  console.log(`${String(val)} is falsy: ${!val}`); // all true
});

// The trap that catches experienced developers: empty array and object are TRUTHY
const emptyCart = [];
const emptyUserProfile = {};
if (emptyCart) console.log('Cart exists (truthy)');         // this runs
if (emptyUserProfile) console.log('Profile exists (truthy)'); // this runs too

// To actually check whether an array has items, check its length
if (emptyCart.length === 0) console.log('Cart is empty'); // this is the correct check

// And for objects, use Object.keys()
if (Object.keys(emptyUserProfile).length === 0) console.log('Profile has no fields');
Output
You are 25 years old
string
Wrong total: 495
Right total: 44
Multiplied: 98
0 is falsy: true
is falsy: true
null is falsy: true
undefined is falsy: true
NaN is falsy: true
false is falsy: true
Cart exists (truthy)
Profile exists (truthy)
Cart is empty
Profile has no fields
Watch Out: HTML Inputs Are Always Strings
Every value you read from a form input — even if the user typed a number, even if the input has type='number' — arrives in JavaScript as a string. Using + to add it to a number will concatenate, not add. The fix is simple: convert at the boundary with Number(inputValue) or parseInt(inputValue, 10) the moment you read from the DOM, before the value touches any arithmetic. Don't convert mid-expression — convert once, validate once, then operate.
Production Insight
The + operator is the only arithmetic operator that favours strings — if either operand is a string, it concatenates rather than adds. Every other operator (-,*,/,%) coerces to number. This is why subtracting a string-number from another string-number 'works' while adding them doesn't — and why the bug hides long enough to reach production.
Rule: convert form inputs to number at the boundary. Never let a raw input value touch a + operator.
Key Takeaway
+ concatenates when either side is a string — -, *, / always coerce to number.
The six falsy values are 0, '', null, undefined, NaN, false — memorise the list.
Empty arrays and objects are truthy — check .length or Object.keys().length, never the container itself.

== vs === : Why Loose Equality Is a Loaded Gun

This is where type coercion gets genuinely dangerous. The loose equality operator == compares values after coercing them to a common type using an algorithm defined in the ECMAScript specification. The strict equality operator === never coerces — if the types don't match, it returns false immediately, no further evaluation.

The coercion rules for == follow a specific decision tree. When comparing a number to a string, the string converts to number. When comparing a boolean to anything, the boolean converts to number first — true becomes 1, false becomes 0 — then that number gets compared. When comparing null to undefined, they're defined as equal by the spec. When comparing null to anything else — 0, '', false — the result is false, which breaks assumptions people make about how null behaves.

The practical takeaway I give every engineer I work with: use === everywhere. The cognitive overhead of predicting == behaviour is real, it accumulates across a codebase, and it produces bugs that are genuinely hard to track down because there's no error — just a wrong boolean result. The one legitimate exception is the null check idiom: value == null deliberately catches both null and undefined in a single expression. That pattern has a long history in JavaScript, it's well understood, and it occasionally saves you from a verbose null || undefined check. Document it with a comment so the next engineer doesn't 'fix' it to === and silently break the undefined case.

looseVsStrictEquality.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// ─── The == coercion algorithm in action ────────────────────────────────────

// String vs Number: string converts to number
console.log('5' == 5);   // true  — '5' becomes 5
console.log('5' === 5);  // false — different types, comparison stops here

// Boolean vs anything: boolean converts to number FIRST
console.log(true == 1);   // true  — true → 1
console.log(false == 0);  // true  — false → 0
console.log(true == '1'); // true  — true → 1, then '1' → 1, then 1 == 1

// null and undefined: equal to each other, nothing else
console.log(null == undefined); // true  — the spec says so explicitly
console.log(null === undefined); // false — different types
console.log(null == 0);         // false — null only equals undefined with ==
console.log(null == false);     // false — same reason, no coercion for null
console.log(null == '');        // false — still no

// ─── The real-world bug: role ID of 0 fails an admin check ──────────────────
function isUserAdmin(role) {
  // Bug: false coerces to 0, and 0 == 0 is true
  // So a role of 0 incorrectly fails this check
  if (role == false) {
    return false;
  }
  return true;
}

console.log(isUserAdmin(0));       // false — WRONG. 0 is a valid role ID.
console.log(isUserAdmin('admin')); // true  — correct

// Fix: === stops the coercion entirely
function isUserAdminFixed(role) {
  if (role === null || role === undefined) {
    return false;
  }
  return true;
}

console.log(isUserAdminFixed(0));       // true  — correct, 0 is a valid role
console.log(isUserAdminFixed(null));    // false — correct
console.log(isUserAdminFixed('admin')); // true  — correct

// ─── The one acceptable == in production ────────────────────────────────────
function processApiResponse(data) {
  // Intentional: catches both null and undefined from the API response
  // Do not 'fix' this to === null — it would break the undefined case
  if (data == null) {
    console.log('No data received from API');
    return;
  }
  console.log('Processing:', data);
}

processApiResponse(null);      // 'No data received from API'
processApiResponse(undefined); // 'No data received from API'
processApiResponse({ id: 1 }); // 'Processing: { id: 1 }'
Output
true
false
true
true
true
true
false
false
false
false
false — WRONG. 0 is a valid role ID.
true
true
false
true
No data received from API
No data received from API
Processing: { id: 1 }
Pro Tip: The Only Acceptable == in Production
value == null is a well-established idiom that intentionally checks for both null and undefined in one expression. It's not a mistake — it's a deliberate use of the coercion algorithm. When you see it in a codebase, it's almost always intentional. Add a comment explaining that intent: // intentional: catches both null and undefined. That comment is the thing that stops a well-meaning code reviewer from converting it to === and shipping a regression.
Production Insight
The == coercion algorithm is deterministic and documented — but it's non-intuitive enough that most engineers can't predict it reliably under pressure. 0 == false is true. null == 0 is false. These rules don't form a consistent mental model that most people carry around.
Rule: use === everywhere. The one exception is value == null — use it intentionally, comment it explicitly, and don't let a linter flag it if the intent is documented.
Key Takeaway
== silently coerces before comparing — === never does. Default to === in all new code.
The one acceptable == is value == null — it catches both null and undefined intentionally.
If you can't predict the output of a == comparison without looking up the spec, that's a sign to use ===.

Explicit Coercion: Taking Back Control

Explicit coercion is you telling JavaScript exactly what type you need. No guessing, no implicit decisions, no surprises for the next developer reading your code. It's always preferable to implicit coercion in production code because your intent is visible — the conversion is right there in the expression.

There are three conversions you'll reach for regularly: to number, to string, and to boolean. Each has a recommended approach. Number() is more predictable than parseInt() for general numeric conversion because it returns NaN for invalid input rather than silently parsing a partial match. String() is safer than .toString() because it handles null and undefined without throwing a TypeError — something that catches people when they're processing API responses that might not have all fields populated. Boolean() is explicit and readable, though the double-bang !! shorthand is equally valid and common enough that any JavaScript engineer should recognise it immediately.

The area that causes the most production issues is converting to number from user input or API data. Number() will return NaN if the conversion fails — and NaN is a number type, which means typeof NaN === 'number' and none of the normal number checks catch it unless you specifically look for it. Always validate with Number.isNaN() after converting. And be careful with the global isNaN() function — it coerces its argument before checking, so isNaN('') returns false because empty string coerces to 0. That's a validation hole. Use Number.isNaN() exclusively.

explicitCoercion.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// ─── To Number ──────────────────────────────────────────────────────────────
const quantityInput = '42';  // from a form field
const priceInput = '19.99'; // from a form field
const invalidInput = 'abc'; // user typed something that isn't a number

console.log(Number(quantityInput)); // 42
console.log(Number(priceInput));    // 19.99
console.log(Number(invalidInput));  // NaN — signals failure clearly
console.log(Number(''));            // 0  — empty string becomes 0, not NaN — watch this
console.log(Number(true));          // 1
console.log(Number(false));         // 0
console.log(Number(null));          // 0
console.log(Number(undefined));     // NaN

// parseInt vs Number: use parseInt only when trailing characters are acceptable
console.log(parseInt('42px', 10)); // 42  — useful when parsing CSS pixel values
console.log(Number('42px'));       // NaN — strict, won't partially parse

// ─── Safe conversion with validation — do this at every input boundary ───────
function calculateOrderTotal(priceString, quantityString) {
  const price = Number(priceString);
  const quantity = Number(quantityString);

  // NaN is a number type — typeof NaN === 'number' — so you have to check explicitly
  if (Number.isNaN(price) || Number.isNaN(quantity)) {
    throw new Error(`Invalid input: price=${priceString}, qty=${quantityString}`);
  }

  return price * quantity;
}

console.log(calculateOrderTotal('19.99', '3')); // 59.97
// calculateOrderTotal('abc', '3'); // throws: Invalid input

// ─── To String ──────────────────────────────────────────────────────────────
const orderCount = 1024;
const isVerified = true;
const emptyValue = null;

console.log(String(orderCount));  // '1024'
console.log(String(isVerified));  // 'true'
console.log(String(emptyValue));  // 'null'      — safe: null.toString() would throw
console.log(String(undefined));   // 'undefined' — also safe

// Template literals trigger the same coercion and are often cleaner to read
const statusMessage = `Order #${orderCount} verified: ${isVerified}`;
console.log(statusMessage); // 'Order #1024 verified: true'

// ─── To Boolean ─────────────────────────────────────────────────────────────
console.log(Boolean(0));        // false
console.log(Boolean(''));       // false
console.log(Boolean('hello'));  // true
console.log(Boolean([]));       // true — empty array is truthy
console.log(Boolean({}));       // true — empty object is truthy

// Double-bang: same result as Boolean(), widely used in React for conditional rendering
const userToken = '';
console.log(!!userToken); // false — no token, don't render protected content

const adminToken = 'eyJhbGci...';
console.log(!!adminToken); // true — token present, render admin panel

// ─── isNaN vs Number.isNaN — this difference matters in production ───────────
console.log(isNaN('hello'));        // true  — 'hello' coerced to NaN, then checked
console.log(isNaN(''));             // false — '' coerced to 0, which isn't NaN — misleading
console.log(Number.isNaN('hello')); // false — no coercion: 'hello' is a string, not NaN
console.log(Number.isNaN(NaN));     // true  — only the actual NaN value returns true

// The practical implication: empty string slips through isNaN validation
const rawInput = '';
if (isNaN(rawInput)) {
  console.log('Invalid — would reject'); // does NOT run — empty string passes!
}
if (Number.isNaN(Number(rawInput))) {
  console.log('Invalid — would reject'); // also does NOT run — Number('') is 0
}
// So for empty string, you need an explicit check:
if (rawInput.trim() === '' || Number.isNaN(Number(rawInput))) {
  console.log('Invalid input'); // this is the correct guard
}
Output
42
19.99
NaN
0
1
0
0
NaN
42
NaN
59.97
'1024'
'true'
'null'
'undefined'
Order #1024 verified: true
false
false
true
true
true
false
true
true
false
false
true
Invalid input
Watch Out: isNaN vs Number.isNaN
The global isNaN() coerces its argument before checking, which means isNaN('') returns false — because empty string coerces to 0, which is not NaN. This creates a validation gap where an empty string passes a numeric check it should fail. Always use Number.isNaN() after an explicit Number() conversion. It never coerces, and it only returns true for actual NaN. And remember: Number('') is 0, not NaN — so you'll still need a separate empty-string guard if an empty field should be rejected.
Production Insight
Number() returns NaN for genuinely invalid input but returns 0 for empty string — which may or may not be what you want. parseInt() silently parses whatever leading digits it can find, which can mask type errors entirely.
isNaN() coerces before checking — Number.isNaN() never coerces. In input validation, always use Number.isNaN().
Rule: convert at the boundary, validate immediately with Number.isNaN() plus an explicit empty-string check where needed, and never operate on values that haven't been validated.
Key Takeaway
Number() for general conversion — parseInt() only when trailing non-numeric characters are acceptable.
String() handles null and undefined safely — .toString() throws on both.
Number.isNaN() never coerces — isNaN('') is false because ''→0. Always use Number.isNaN().

Coercion in the Wild: Patterns You'll Actually See in Production

Knowing the rules is one thing. Recognising them in real code — in React components, Node.js API handlers, utility libraries — is what separates someone who studied coercion from someone who actually owns it. There are a handful of patterns that show up constantly in production JavaScript and you should be able to read and write them without hesitation.

Default values using the || operator are everywhere. So is the bug they introduce: || treats every falsy value as 'missing', which means a valid 0, a valid empty string, or a valid false gets silently replaced by the default. The nullish coalescing operator ?? was added to the language specifically to fix this — it only triggers on null and undefined. Choosing between them is a real signal of experience in a code review.

Array sorting is a different class of coercion surprise. The default sort() method converts elements to strings before comparing — that's documented behaviour, not a bug. But it means [10, 9, 2, 1, 100].sort() gives you [1, 10, 100, 2, 9] — alphabetical order, not numeric. It's one of those cases where JavaScript is doing exactly what the spec says and producing exactly the wrong result for what you intended.

And in React specifically, there's a well-known rendering trap: {count && <Component />} renders the number 0 on screen when count is 0, because 0 is falsy and the short-circuit returns it rather than false. The fix is {count > 0 && <Component />} — a boolean check, not a truthy check.

coercionInProduction.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// ─── Default values: || vs ?? ────────────────────────────────────────────────

// || returns the right side when the left is falsy
// Bug: 0 is a valid rating but 0 is falsy — it gets replaced
function displayProductRating(rating) {
  const displayRating = rating || 'No rating yet';
  console.log('Rating:', displayRating);
}

displayProductRating(4.5);  // 'Rating: 4.5'            — correct
displayProductRating(0);    // 'Rating: No rating yet'  — WRONG, 0 is valid
displayProductRating(null); // 'Rating: No rating yet'  — correct

// Fix: ?? only replaces null and undefined, preserves valid falsy values
function displayProductRatingFixed(rating) {
  const displayRating = rating ?? 'No rating yet';
  console.log('Rating:', displayRating);
}

displayProductRatingFixed(4.5);  // 'Rating: 4.5'           — correct
displayProductRatingFixed(0);    // 'Rating: 0'             — correct
displayProductRatingFixed(null); // 'Rating: No rating yet' — correct

// ─── The array sort coercion trap ────────────────────────────────────────────
const inventoryQuantities = [10, 9, 2, 1, 100, 20];

// Default sort: coerces to strings, sorts lexicographically
const wronglySorted = [...inventoryQuantities].sort();
console.log('Wrong sort:', wronglySorted); // [1, 10, 100, 2, 20, 9]

// Fix: provide a numeric comparator
const correctlySorted = [...inventoryQuantities].sort((a, b) => a - b);
console.log('Correct sort:', correctlySorted); // [1, 2, 9, 10, 20, 100]

// ─── Short-circuit evaluation in React (boolean coercion) ────────────────────
const cartItems = [];         // empty array — truthy
const notificationCount = 0; // zero — falsy
const userName = 'Alex';      // non-empty string — truthy

// cartItems is truthy, so cartItems.length > 0 is the correct guard
const showCartBadge = cartItems.length > 0 && '<CartBadge />';
const showNotificationDot = notificationCount > 0 && '<NotificationDot />';
const showWelcomeMessage = userName && `<WelcomeMessage name="${userName}" />`;

console.log(showCartBadge);        // false — correct, no badge
console.log(showNotificationDot);  // false — correct, no dot
console.log(showWelcomeMessage);   // '<WelcomeMessage name="Alex" />' — correct

// ─── The React 0-rendering trap ──────────────────────────────────────────────
// {0 && <Component />} doesn't render nothing — it renders the number 0 on screen
// because the short-circuit returns 0, which React renders as a text node
const itemCount = 0;
console.log(itemCount && 'Show list');      // 0   — React renders '0' on screen
console.log(itemCount > 0 && 'Show list'); // false — React renders nothing

// This affects any counter or numeric flag that can be 0:
const pendingOrders = 0;
// Bad:  {pendingOrders && <OrderList />}  — renders '0' on screen
// Good: {pendingOrders > 0 && <OrderList />}  — renders nothing
Output
Rating: 4.5
Rating: No rating yet
Rating: No rating yet
Rating: 4.5
Rating: 0
Rating: No rating yet
Wrong sort: [1, 10, 100, 2, 20, 9]
Correct sort: [1, 2, 9, 10, 20, 100]
false
false
<WelcomeMessage name="Alex" />
0
false
Pro Tip: Use ?? for API Response Defaults
When pulling default values from API responses, configuration objects, or user settings, prefer ?? over || for any field that can legitimately be 0, false, or an empty string. A product price of 0 means free — not missing. A timeout of 0 means no timeout — not 'use the default'. Using || for these fields replaces valid data silently, and the resulting bug can be extremely hard to reproduce because it only manifests when the value is specifically a valid falsy. Switch to ?? and you don't have to think about it.
Production Insight
|| treats 0, false, and '' as 'missing' — ?? only treats null and undefined as missing. In any codebase that handles numeric config values, prices, counts, or user-entered data, || in a default expression is a latent bug waiting for someone to set a value to 0.
Default sort() coerces to strings — it's in the spec, it's consistent, and it's still wrong for numeric arrays every time.
In React, always use a boolean expression (count > 0) rather than a truthy check (count) when short-circuiting rendering with a numeric value. The 0-rendering bug is subtle and common.
Key Takeaway
|| replaces all falsy values — ?? only replaces null and undefined. Use ?? for any default where 0 or '' is a valid value.
Default sort() coerces to strings — always provide a numeric comparator for numeric arrays.
In React, {0 && <Component />} renders '0' on screen — use .length > 0 or an explicit boolean expression.
● Production incidentPOST-MORTEMseverity: high

E-Commerce Checkout Calculated $0.00 for Free Products When Price Was Actually $49.99

Symptom
Customers reported seeing $0.00 or wildly incorrect totals in the checkout summary. Some orders processed at the wrong price entirely — we caught it through a spike in refund requests two days after a frontend deploy, not through monitoring. By the time we traced it, a non-trivial number of orders had gone through at incorrect totals.
Assumption
The team assumed that HTML input values typed as numbers — '49.99', '10', '5' — would behave as JavaScript numbers when used in arithmetic. The original code had worked fine in manual testing because the happy path never produced an obviously wrong result, just a subtly wrong one.
Root cause
The checkout total was calculated as priceInput.value + discountInput.value - taxInput.value. All three values came from HTML text inputs, which always return strings — no exceptions. The + operator encountered two strings and did exactly what it's supposed to do: it concatenated them. '49.99' + '10' produced '49.9910'. Then the - operator, which coerces to number, gave us '49.9910' - '5' = 44.991 — wrong, but not obviously broken. The edge case that surfaced in production was an optional discount field left empty: '49.99' + '' produced '49.99', then '49.99' - '5' = 44.99, which looked almost right. But when the tax field was also empty: '49.99' + '' - '' triggered '49.99' - '' = 49.99, then the subsequent calculation produced NaN, which rendered as $0.00 in the price formatter.
Fix
1. Added Number() conversion at the input boundary — const price = Number(priceInput.value) — the moment we read from the DOM, not mid-expression 2. Added explicit NaN validation before any arithmetic: if (Number.isNaN(price)) throw new Error('Invalid price input') 3. Created a shared parseCurrency(value) utility that handles the conversion, validation, and a sensible error message in one place — no more ad-hoc Number() calls scattered through the codebase 4. Added unit tests with the edge cases that bit us: empty string, '0', ' ' (whitespace), negative numbers, and strings like '49.99abc' that parseInt would have partially parsed
Key lesson
  • HTML input values are ALWAYS strings — it doesn't matter what the user typed or what type attribute you set on the input
  • Convert at the boundary: Number() or parseInt() the moment you read a value from the DOM, before it touches any arithmetic
  • Always validate with Number.isNaN() immediately after conversion — do not assume the conversion succeeded just because the input looked numeric
  • The + operator is the only arithmetic operator that favours strings — subtraction (-) saved us from noticing the bug earlier because it coerced back to number
  • Unit test arithmetic functions with string inputs explicitly — the type system won't save you here
Production debug guideWhen your JavaScript produces unexpected results from operations that should be simple4 entries
Symptom · 01
Arithmetic produces string concatenation instead of numeric addition
Fix
Check if either operand is a string. Run typeof on both values in the console before the operation. If either is 'string', convert with Number() before operating. The + operator will concatenate the moment it sees a string on either side — there's no warning.
Symptom · 02
Comparison returns wrong boolean — e.g., 0 == false is true, or a valid role ID of 0 fails an auth check
Fix
Replace == with ===. If you're seeing unexpected false negatives for falsy values like 0 or empty string, that's the coercion algorithm biting you. The only legitimate == in production is value == null — anything else, switch to strict equality.
Symptom · 03
A default value is replacing a valid 0 or an empty string that should be preserved
Fix
Replace || with ?? (nullish coalescing). The || operator treats every falsy value as 'missing' — 0, false, and '' all trigger the fallback. The ?? operator only triggers on null and undefined. If a value can legitimately be 0 or empty string, ?? is the right tool.
Symptom · 04
Array sort produces wrong order — [10, 9, 2, 100] sorts as [1, 10, 100, 2] — or prices appear in alphabetical rather than numeric sequence
Fix
The default sort() method coerces every element to a string before comparing. Provide a comparator: .sort((a, b) => a - b) for ascending numeric order. This is one of those places where coercion is working exactly as documented — it just isn't what you want.
Implicit vs Explicit Coercion
AspectImplicit CoercionExplicit Coercion
Who controls itJavaScript engine — based on operator and type rulesYou — intentional conversion written in code
ReadabilityOpaque — reader must know the coercion rules to understand the expressionClear — the conversion is visible and the intent is obvious
Debugging difficultyHard — silent, no error thrown, result looks plausibleEasier — failure surfaces as NaN or TypeError, not a silent wrong value
Operator examples+ - * / == if() while() || ternaryNumber() String() Boolean() parseInt() parseFloat()
When appropriateTemplate literals, documented falsy checks, short-circuit rendering guardsForm input processing, API response parsing, any type-sensitive arithmetic
Risk levelHigh — easy to produce surprising results with no warningLow — predictable, auditable, testable
null/undefined handlingVaries by context and operator — often counterintuitiveString(null)='null', Number(null)=0 — consistent and documented
Team code reviewRequires everyone to carry the same mental model of coercion rulesSelf-documenting — no shared mental model required to read it correctly

Key takeaways

1
The + operator favours strings
if either operand is a string, it concatenates. Every other arithmetic operator (-,*,/,%) favours numbers. This single asymmetry explains most of the coercion bugs you'll encounter with form inputs and API data.
2
Use === everywhere by default. The only well-accepted exception is value == null, which deliberately catches both null and undefined in one expression
always comment it when you use it intentionally.
3
Use ?? instead of || for default values whenever 0, false, or '' are valid in the domain. The || operator treats all falsy values as missing; ?? only triggers on null and undefined.
4
Number.isNaN() and isNaN() are not interchangeable. The global isNaN() coerces before checking
isNaN('') is false because '' coerces to 0. Always use Number.isNaN(). And remember: Number('') is 0, not NaN, so you still need an explicit empty-string guard if blank fields should be rejected.
5
HTML input values are always strings
convert with Number() or parseInt() at the DOM boundary, validate with Number.isNaN(), then operate. Never let a raw .value touch arithmetic.
6
Default sort() coerces elements to strings before comparing
always provide a numeric comparator for arrays of numbers, prices, or version identifiers.

Common mistakes to avoid

5 patterns
×

Adding a number to a raw form input value with the + operator

Symptom
total = priceInput.value + quantityInput.value produces '2550' instead of 75 when price is '25' and quantity is '50'. The checkout total is wrong and the error is silent — no exception, just a concatenated string that looks like a number.
Fix
Convert both inputs immediately after reading from the DOM: const price = Number(priceInput.value); const quantity = Number(quantityInput.value). Then validate with Number.isNaN() before proceeding. Never let a raw .value touch a + operator.
×

Using || for default values when 0, false, or '' are valid in the domain

Symptom
const timeout = userSetting || 3000 replaces a user's intentional setting of 0 (no timeout) with 3000. Free products show 'N/A' instead of '$0.00' because price || 'N/A' fires on a valid 0. The bug only surfaces for specific valid values and can be hard to reproduce.
Fix
Use the nullish coalescing operator ??: const timeout = userSetting ?? 3000. It only falls back when userSetting is null or undefined, leaving 0, false, and '' untouched. Audit existing || default expressions in any code that handles prices, counts, or configuration values.
×

Using the global isNaN() to validate numeric input from forms or API responses

Symptom
isNaN('') returns false because empty string coerces to 0 before the check runs. Empty fields pass numeric validation and reach arithmetic operations, producing NaN downstream — usually far from the point where the input was read.
Fix
Always use Number.isNaN() after explicitly converting the value: const parsed = Number(input); if (parsed.trim() === '' || Number.isNaN(parsed)) — handle the error. Add an explicit empty-string guard because Number('') is 0, not NaN, which means Number.isNaN alone won't catch blank fields.
×

Using == instead of === in comparisons involving role IDs, status codes, or type checks

Symptom
0 == false is true, so a role ID of 0 incorrectly fails an admin check. '200' == 200 is true, masking an HTTP status comparison where the type mismatch indicates a real data problem upstream. Bugs are intermittent and correlate with specific values, making them hard to reproduce.
Fix
Use === everywhere by default. The only documented exception is value == null to catch both null and undefined — add a comment when you use it intentionally. If you're comparing values that might be different types, that type mismatch is usually information you want to act on, not suppress.
×

Relying on default sort() for arrays of numbers, prices, or version strings

Symptom
[10, 9, 2, 1, 100].sort() returns [1, 10, 100, 2, 9] — alphabetical, not numeric. Inventory quantities appear in nonsensical order. Version numbers sort incorrectly. The bug is consistent and reproducible, but tends to surface in QA rather than development because test data often uses small integers that happen to sort correctly.
Fix
Always provide a comparator for numeric arrays: .sort((a, b) => a - b) for ascending order. For descending: .sort((a, b) => b - a). For strings, the default sort() works correctly without a comparator. Make it a code review checkpoint: any .sort() on a numeric array without a comparator is a bug.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What's the difference between == and === in JavaScript, and can you desc...
Q02JUNIOR
What does '5' - 3 evaluate to, and why does '5' + 3 give a different res...
Q03SENIOR
If I write if ([]) { console.log('truthy') }, will that log? What about ...
Q04SENIOR
What's the difference between isNaN() and Number.isNaN(), and why does i...
Q05SENIOR
When should you use ?? instead of || for default values? Give a concrete...
Q01 of 05JUNIOR

What's the difference between == and === in JavaScript, and can you describe a real scenario where using == would produce a bug that === would prevent?

ANSWER
== performs type coercion before comparing — it converts operands to a common type using the ECMAScript abstract equality algorithm. === never coerces — if the types don't match, it returns false immediately with no further evaluation. A concrete bug: if (role == false) returns true when role is 0, because false coerces to 0 and 0 == 0 is true. If 0 is a valid role ID in your system, that check incorrectly rejects it. Using role === false correctly returns false for role=0 because number !== boolean, no coercion runs.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is type coercion in JavaScript and why does it happen?
02
Is it ever okay to use == instead of === in JavaScript?
03
Why is an empty array truthy in JavaScript when [] == false is also true? That seems like a contradiction.
04
What is the difference between Number() and parseInt() for converting strings to numbers?
🔥

That's JS Basics. Mark it forged?

6 min read · try the examples if you haven't

Previous
Conditionals in JavaScript
10 / 16 · JS Basics
Next
Scope and Hoisting in JavaScript