skip to content

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?

level: seniorimportance: should knowfreq 36%

answer

  1. contagious value with no provenance
  2. check the inputs, not the total
  3. undefined arithmetic is the usual culprit
  4. nullish coalescing will not catch it
  5. one finite check at the parse boundary

basics

~20 s

NaN 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 s

NaN 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 lines
javascript
const 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 0

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context