A Go service's financial report renders NaN in every margin cell after one bad row. How do you find the source?
answer
- it spreads through every operation
- every comparison against it is false
- the threshold check never fired
- instrument the stages, not the renderer
- print the raw bits of the suspect value
basics
~20 sNaN is contagious and silent: any arithmetic touching it yields NaN, and every comparison against it is false, so a threshold check never fires. Instrument stage boundaries with math.IsNaN and log math.Float64bits, then fix the division that made it.
solid answer
~50 sWork backwards, and do not start at the renderer. A NaN spreads through every arithmetic operation it touches, so one bad ratio poisons its row, the column total and any aggregate built from them. It also hides: for a NaN `v` both `v > limit` and `v <= limit` are false, so the sanity check that was supposed to catch outliers silently took neither branch. Bisect the pipeline by asserting `math.IsNaN(v) || math.IsInf(v, 0)` at each stage boundary — after decode, after each derived ratio, before aggregation — and log the offending record's key together with `math.Float64bits(v)`, which shows unambiguously what the value is. In practice the origin is a division where numerator and denominator are both zero: an account with zero revenue, a window with no samples. Fix it at that division by treating the zero denominator as a named domain case, and keep the guard so the non-finite value can never be stored again.
code
go · 3 linesbad := math.NaN()
fmt.Println(bad > 1.0, bad <= 1.0, bad == bad)
// prints: false false falsego deeper
Focus on the two facts that drive everything else: arithmetic involving NaN yields NaN, and comparisons against NaN are always false. math.IsNaN is how you detect one.
Be able to trace the propagation — which operation produced the first NaN, how it reached the total, and why the existing validation branch did not fire — and to name the usual sources such as zero over zero and math.Sqrt of a negative.
Show a real diagnostic method: instrument stage boundaries with IsNaN and IsInf, log math.Float64bits for the suspect value, reproduce with a test row, and put the fix at the division rather than at the render.
Own the standing rule that comes out of the postmortem: which values may be floats, that finiteness is validated separately from range at every ingest boundary, and how that check is made routine rather than added incident by incident.
### Why a single bad row takes out a whole report Two properties of NaN combine badly. **It is contagious.** Every arithmetic operation with a NaN operand produces NaN. A per-row margin of `profit / revenue` where both are zero is NaN; add that row into a column total and the total is NaN; compute an average, a variance or a weighted score from that total and they are NaN too. One row can therefore render as NaN everywhere without any other row being wrong. **It is silent.** IEEE 754 makes every comparison involving NaN false — `<`, `<=`, `>`, `>=` and `==` all return false, and only `!=` returns true. This is the part that turns a bug into an incident. A guard like `if margin > 1.0 { flag() }` does not fire; neither does `if margin <= 1.0 { ok() }`. Code that looked defensively written passed the value straight through, and nothing was logged at the moment it appeared. ### Where the NaN actually entered The short list, in the order they show up in practice: 1. **Zero over zero.** A ratio whose numerator and denominator are both zero — no revenue, no samples, no rows in the period. A nonzero numerator over zero gives an infinity instead, which then turns into NaN at the first subtraction of two infinities. 2. **Domain errors in `math`.** `math.Sqrt` of a negative number and `math.Log` of a non-positive number both return NaN rather than an error. 3. **Parsed input.** `strconv.ParseFloat` accepts the literal texts `"NaN"`, `"Inf"` and `"-Inf"`, so a CSV column or a query parameter can hand you one with no arithmetic involved. ### Finding it Do not debug the renderer; it is faithfully printing what it was given. Bisect the pipeline instead: - Add `math.IsNaN(v) || math.IsInf(v, 0)` at each stage boundary — after decode, after each derived value, before aggregation — and make the check log the record identifier alongside the value. The first stage that reports is the one to open. - Log `math.Float64bits(v)` for the suspect value. Raw bits remove all ambiguity: a NaN has every exponent bit set with a non-zero fraction, an infinity has every exponent bit set with a zero fraction, and it even distinguishes negative zero from zero, which the formatted output does not. - Use the serialiser as a tripwire. `encoding/json` refuses to marshal a NaN or an infinity and returns a `*json.UnsupportedValueError`, because JSON has no representation for them. If part of your pipeline encodes JSON, that error is a free, loud detector at a known point — and if the report path does not encode JSON, this is a reason to add an explicit finiteness check where it would have been. - Reproduce with a table-driven test containing the offending row. Zero revenue, zero profit, and empty periods make excellent permanent test cases, because they are the inputs nobody generates by hand. ### Fixing it, in the right place The fix belongs at the division, not at the display. Replacing NaN with zero in the renderer makes the report look right and destroys the evidence that a period had no revenue, which is usually itself worth reporting. - Treat the zero denominator as a domain case with a name: return `(0, false)`, a sentinel, or an error, and let the caller decide whether the cell is `n/a`, `0` or an alert. - Validate on the way in. One `math.IsNaN` check at the ingest boundary keeps the value out of storage entirely, which matters because a persisted NaN outlives the deploy that fixed the code. - Keep exact quantities exact. Money as integer minor units cannot become NaN or infinite; only the derived ratios can, which shrinks the surface you have to guard to a handful of divisions. - Beware aggregations. A sum containing one NaN is NaN, and sorting behaves oddly too — `slices.Sort` on a `[]float64` places NaNs before all other values, so they cluster at the front rather than corrupting the order arbitrarily. ### What the writeup should say The honest postmortem line is not "a division by zero" — it is that the value's own comparison rules disabled the validation that existed. That is the reusable lesson: for floating-point results, *finiteness is a separate check from range*, and only `math.IsNaN` and `math.IsInf` perform it.
- Why is a NaN in a report worse than a panic would have been?A panic names the line and stops the damage. A NaN keeps going: it propagates into totals and averages, it survives validation because every comparison against it is false, and it surfaces far from where it was created — often in a rendered cell, hours later, in front of a customer. The cost of the incident is almost entirely the distance between origin and symptom.
- Can encoding/json marshal a float64 holding NaN?No. JSON has no literal for NaN or infinity, so `json.Marshal` returns a `*json.UnsupportedValueError`. That makes an encoding step a useful tripwire — it converts a silent value into a loud error at a known boundary — but it also means the failure surfaces at serialisation rather than at the division, so keep the check at the source as well.
- What ordering do NaNs get when you sort a []float64 with slices.Sort?They are placed before every other value. `slices.Sort` orders floating-point values with NaNs first, so they gather at the front instead of scattering unpredictably. It makes them easy to spot in sorted output, but it is not a substitute for filtering: a `min` or a sum computed over the same slice is still contaminated.
NaN behaves like a contaminated sample that also disables the detector: it spreads into everything it is mixed with, and every test you run on it comes back negative.
saying these in an interview costs you the question
- Writes the guard as if v == math.NaN()
- Believes a threshold check such as v > limit catches a NaN
- Blames the renderer instead of the division that produced it
- Replaces the NaN with zero at display time and ships
- Assumes a bad division would have panicked, so this cannot be one