A JavaScript dashboard computes a total from API data and intermittently renders it as NaN. How do you find where the NaN came from, and how do you stop this class of bug?
answer
- contagious value with no provenance
- check the inputs, not the total
- undefined arithmetic is the usual culprit
- nullish coalescing will not catch it
- one finite check at the parse boundary
basics
~20 sNaN is contagious and carries no provenance: one bad field poisons every downstream result. Bisect by testing each input with Number.isFinite instead of inspecting the total, then validate and reject non-finite values at the boundary where data enters.
solid answer
~50 sNaN propagates through every arithmetic operation it touches, so by the time it reaches the UI the total tells you nothing about which input was bad. I stop inspecting the total and start inspecting the inputs: filter the source rows with `!Number.isFinite(Number(row.amount))` to surface the offending records, which usually reveals the real cause — a renamed or missing field so the value is `undefined`, a string like `"12px"` or `"1,200"` that `Number` rejects, or a `0/0` in a derived ratio. The permanent fix is validation at the boundary: convert once where the payload is parsed, reject or default non-finite values there, and log the offending field while you still know its name. I prefer `Number.isFinite` over a NaN check because it also catches an overflowed `Infinity`, and I avoid `value ?? 0` as a guard, since NaN is not nullish and slips straight through it.
code
javascript · 11 linesconst rows = [{ amount: 10 }, { amount: "12px" }, { amount: 3 }];
const total = rows.reduce((sum, r) => sum + Number(r.amount), 0);
console.log(total); // NaN — one bad row poisons the whole sum
// Find the culprit instead of inspecting the total
const bad = rows.filter(r => !Number.isFinite(Number(r.amount)));
console.log(bad); // [ { amount: '12px' } ]
console.log(NaN ?? 0); // NaN — nullish coalescing does not catch it
console.log(NaN || 0); // 0 — but this also rewrites a real 0go deeper
Be able to say that any arithmetic involving NaN produces NaN, and name a likely source such as adding an undefined property. Knowing to check the inputs is enough at this level.
Explain the concrete producers — undefined arithmetic, Number("12px"), 0/0 on an empty set — and show a Number.isFinite filter that identifies the offending record.
Describe the boundary discipline: convert and validate once at parse time, attach the field name to the failure, and choose deliberately between throwing and a counted fallback so the incident is visible.
Own the contract across services: whether non-finite values are rejected at the edge or represented explicitly, given that JSON flattens NaN and Infinity to null and destroys the evidence at the boundary.
## Why NaN is hard to debug NaN has two properties that make it a uniquely annoying production value. First, it is **contagious**: every arithmetic operation with a NaN operand produces NaN, so a single bad element turns a whole `reduce` into NaN. Second, it carries **no provenance**: the final NaN is identical whether it came from a missing field, an unparsable string, or a `0/0` twelve functions upstream. Compounding both, nothing throws — the bad value travels all the way to the render layer before anyone notices. ```js const rows = [{ amount: 10 }, { amount: "12px" }, { amount: 3 }]; rows.reduce((sum, r) => sum + Number(r.amount), 0); // NaN ``` ## Step 1 — stop looking at the output The total is the last place with useful information. Move the check to the inputs and let it name the culprit: ```js const bad = rows.filter(r => !Number.isFinite(Number(r.amount))); console.log(bad); // [ { amount: '12px' } ] ``` If the pipeline has several stages, insert the same predicate between stages and bisect: the first stage whose output contains a non-finite value is where the problem lives. In a hot loop, a temporary `if (!Number.isFinite(value)) console.warn(key, raw)` beats a breakpoint, because the failure is usually data-dependent and intermittent. ## Step 2 — recognise the usual sources In practice the NaN nearly always comes from one of these: - **A missing or renamed property.** `undefined + 1` is NaN, so a typo'd key or an API field rename produces it instantly. This is the single most common cause and the reason the bug is intermittent — only some records lack the field. - **A string that `Number` refuses.** `Number("12px")`, `Number("1,200")` and `Number("$5")` are all NaN. Note the asymmetry with `parseFloat`, which returns `1` for `"1,200"` — quietly wrong instead of loudly NaN. - **`null` versus `undefined`.** `Number(null)` is `0` and `Number("")` is `0`, but `Number(undefined)` is NaN. So a nullable column is usually harmless while an absent property is not, which explains why the same code works against one payload and not another. - **Indeterminate arithmetic.** A derived ratio computed as `part / total` gives NaN when both are zero — an empty result set is a very common trigger. - **Invalid dates.** Arithmetic on a `Date` built from an unparsable string produces NaN, because its time value is NaN. ## Step 3 — validate at the boundary The durable fix is to convert and validate exactly once, where external data enters the system, and to fail loudly there: ```js function toAmount(raw, field) { const n = typeof raw === "string" ? Number(raw.trim()) : raw; if (!Number.isFinite(n)) { throw new TypeError(`Non-finite value for ${field}: ${JSON.stringify(raw)}`); } return n; } ``` Three deliberate choices there. `Number.isFinite` rather than `Number.isNaN`, because an overflowed or divided-by-zero value is `Infinity`, which is not NaN and would pass a NaN-only check. The field name in the message, because that is the provenance the NaN itself destroyed. And a thrown error rather than a silent default, so the failure is attributed to the parse step. Where throwing is too aggressive — a dashboard that must still render — return a typed "unavailable" marker and count the drops, so the incident is visible in metrics rather than as a NaN on a chart. ## The guards that do not work Two popular one-liners are traps: - `value ?? 0` does **not** protect you. Nullish coalescing only replaces `null` and `undefined`; NaN is neither, so it passes straight through. - `value || 0` does catch NaN, since NaN is falsy — but it also rewrites a legitimate `0` and an empty string, so it silently corrupts valid data while appearing to fix the bug. The explicit form says what you mean: `Number.isFinite(value) ? value : fallback`. ## What happens if it escapes A NaN that reaches serialization does not stay recognisable: `JSON.stringify` writes NaN and both infinities as `null`. So a NaN produced in one service arrives at the next one as a null, indistinguishable from a genuinely missing value, and the trail goes cold at the boundary. That alone justifies validating before you serialize, not after. Finally, add a regression test with the exact bad record — a fixture containing the `undefined` field or the `"12px"` string — asserting that the parser rejects it. NaN bugs recur because the offending shape reappears in the next payload version.
- Why is Number.isFinite the right guard here rather than Number.isNaN?Because NaN is not the only bad outcome. An overflow or a division by zero yields `Infinity`, which is a perfectly ordinary Number and passes a NaN-only check, then renders as "Infinity" or serializes to null. `Number.isFinite` rejects NaN, both infinities and non-numbers in one call, which is exactly the property a numeric field needs.
- A colleague fixes the bug with amount ?? 0. Why is that the wrong fix?Nullish coalescing only substitutes for `null` and `undefined`. NaN is neither, so a value that is already NaN — from an unparsable string or upstream arithmetic — passes through untouched and the total is still NaN. It does mask the `undefined`-property case, which makes it look like it worked while leaving the string case live.
- The API returns amounts as strings and some contain thousands separators. What is the risk of switching to parseFloat?`parseFloat("1,200")` returns `1`, because it reads the longest valid prefix and stops at the comma. That converts a loud NaN into a silently wrong number, which is far worse in a financial total. Keep the all-or-nothing behaviour of `Number`, and strip separators explicitly with a documented rule before converting.
saying these in an interview costs you the question
- Fixes it by checking the final total instead of the inputs
- Uses value ?? 0, which never fires for NaN
- Uses value || 0, which also rewrites legitimate zeros
- Guards only for NaN and lets Infinity through
- Assumes the NaN was thrown somewhere and looks for an error