JavaScript isNaN Accepts Empty Strings: Validate Required Numbers
Contents
If a required amount field uses !isNaN(raw) as its validation check, an empty field can pass and arrive in your JSON request as {"amount":0}. An entered zero produces the same payload. Check for blank text before converting it, then check that the converted value is finite.
Where the blank field becomes zero
Global isNaN() converts its argument before testing for NaN. The string-to-number rules give empty and whitespace-only strings the numeric value zero. By the time you inspect Number(raw), the distinction between an empty field and an entered zero has already been lost.
Number.isNaN(raw) also returns false for blank text. It checks whether its argument is already the numeric value NaN, so it returns false for ordinary numeric strings and malformed strings alike. Even after conversion, a check for NaN still lets infinity through. In the fixture below, '1e309' passes the original gate and becomes {"amount":null} under the JSON serialization rules for non-finite numbers.
Validate the raw field before building the request
For a required text field, reject missing or non-text input, trim the string, then reject an empty result. Convert the remaining text with Number() and use Number.isFinite() to reject both failed conversions and infinite values. If the parser returns an error, surface it at the field. Only build the outgoing amount payload on success. A truthiness check on amount would reject a valid zero.
This parser accepts JavaScript numeric notation, including '1e3' and '0x10'. If your field requires a particular decimal format, validate that format on the trimmed text before conversion. The finite check addresses numeric validity, while allowed notation, range and precision depend on the field’s contract. For the separate problem of parseInt consuming a numeric prefix or receiving an unexpected radix, check the parsing call itself.
Reproduce the payload collision
Save the fixture as amount-check.mjs and run node amount-check.mjs. The example was verified on macOS with Node.js 24.13.0 using raw JavaScript values. It exercises the parser and payload serialization without a browser or server.
import assert from 'node:assert/strict';
function parseAmount(raw) { if (typeof raw !== 'string') return { error: 'expected text' };
const text = raw.trim(); if (text === '') return { error: 'required' };
const amount = Number(text); if (!Number.isFinite(amount)) return { error: 'expected a finite number' };
return { amount };}
const cases = [ ['', { error: 'required' }], [' \t', { error: 'required' }], ['0', { amount: 0 }], [' 2.5 ', { amount: 2.5 }], ['12px', { error: 'expected a finite number' }], ['Infinity', { error: 'expected a finite number' }], ['1e309', { error: 'expected a finite number' }], ['1e3', { amount: 1000 }], ['0x10', { amount: 16 }], [null, { error: 'expected text' }],];
for (const [input, expected] of cases) { assert.deepEqual(parseAmount(input), expected);}
function uncheckedBody(raw) { return isNaN(raw) ? undefined : JSON.stringify({ amount: Number(raw) });}
assert.equal(uncheckedBody(''), '{"amount":0}');assert.equal(uncheckedBody('0'), '{"amount":0}');assert.equal(uncheckedBody('1e309'), '{"amount":null}');assert.equal(JSON.stringify(parseAmount('0')), '{"amount":0}');console.log('10 parser cases and 4 payload checks passed');The command prints 10 parser cases and 4 payload checks passed. The original gate produces identical JSON for blank text and an entered zero. The parser produces a field error for blank text and preserves zero through serialization.
References: