JavaScript Type Coercion — Empty Inputs Cause $0.00 Bug
Empty input fields caused + to concatenate strings, producing NaN that displayed as $0.
- 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
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.
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.
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.
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.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.Number.isNaN() never coerces. In input validation, always use Number.isNaN().Number.isNaN() plus an explicit empty-string check where needed, and never operate on values that haven't been validated.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.
sort() coerces to strings — it's in the spec, it's consistent, and it's still wrong for numeric arrays every time.sort() coerces to strings — always provide a numeric comparator for numeric arrays.E-Commerce Checkout Calculated $0.00 for Free Products When Price Was Actually $49.99
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- 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
Number() before operating. The + operator will concatenate the moment it sees a string on either side — there's no warning.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.Key takeaways
Number.isNaN() and isNaN() are not interchangeable. The global isNaN() coerces before checkingNumber.isNaN(). And remember: Number('') is 0, not NaN, so you still need an explicit empty-string guard if blank fields should be rejected.Number() or parseInt() at the DOM boundary, validate with Number.isNaN(), then operate. Never let a raw .value touch arithmetic.sort() coerces elements to strings before comparingCommon mistakes to avoid
5 patternsAdding a number to a raw form input value with the + operator
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
Using the global isNaN() to validate numeric input from forms or API responses
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
Relying on default sort() for arrays of numbers, prices, or version strings
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 Questions on This Topic
What's the difference between == and === in JavaScript, and can you describe a real scenario where using == would produce a bug that === would prevent?
Frequently Asked Questions
That's JS Basics. Mark it forged?
6 min read · try the examples if you haven't