JAVASCRIPT / NUMBERS, MATH, AND BIGINT
Parsing numbers from strings safely
Convert user-typed strings into trustworthy numbers by choosing between Number, parseInt, and parseFloat, then validating the result with Number.isFinite.
What you will learn
- Use Number(text.trim()) with Number.isFinite to accept only fully numeric strings
- Explain why parseInt('12px') is 12 but Number('12px') is NaN
- Always pass a radix to parseInt, as in parseInt(text, 10)
- Catch the traps: Number('') is 0, Number(null) is 0, Number('0x1f') is 31
Understanding Parsing numbers from strings safely
JavaScript gives you two different contracts for turning text into a number, and most parsing bugs come from picking the wrong one. Number(text) and unary + are whole-string converters: after optional surrounding whitespace, every remaining character must fit the numeric literal grammar, or the result is NaN. parseInt and parseFloat are prefix scanners: they skip leading whitespace, consume as many characters as they can, and return whatever they built while discarding the rest. That is why Number('12px') is NaN while parseInt('12px', 10) is 12 — one is reporting that the input is malformed, the other is guessing what you meant.
Failure has exactly one representation here, the number NaN, and NaN is the only value not equal to itself, so you cannot test for it with ===. Number.isNaN(n) is the direct test, but Number.isFinite(n) is usually the better gate because it also rejects Infinity, which arrives from strings like 'Infinity' and '-1e400'. Avoid the global isNaN, which converts its argument before testing, so isNaN('') is false. That zero is the second trap: the empty string, a whitespace-only string, null, false and [] all convert to 0, so a blank field can reach your code as a legitimate-looking quantity.
For text typed by a person, the safest structure is to decide the format you accept, verify it, and only then convert. An anchored regular expression is a good gate because it states the grammar of your field instead of inheriting JavaScript's, which also allows hex literals, exponents and Infinity. Reach for parseFloat only when trailing characters are genuinely expected, such as reading '18.5px' out of a stylesheet, and pass the radix to parseInt every time, since without it a leading 0x still switches the base to 16. Return an explicit failure value such as null, because NaN spreads quietly through arithmetic and surfaces much later as a broken total.
Numbers are important.
function toNumber(input) {
const text = String(input).trim();
if (text === "") return NaN;
const n = Number(text);
return Number.isFinite(n) ? n : NaN;
}
const raws = ["42", " 3.5 ", "", "12px", "1e3", "0x1f", "Infinity"];
for (const raw of raws) {
console.log(`${JSON.stringify(raw)} -> Number: ${toNumber(raw)}, parseInt: ${parseInt(raw, 10)}`);
}Number is an all-or-nothing converter while parseInt and parseFloat are lenient prefix scanners, so safe parsing means using the strict one and then verifying the result with Number.isFinite.
Worked examples
What the radix actually controls
Shows that parseInt's second argument picks the digit alphabet, not the strictness of the scan.
console.log(parseInt("ff", 16));
console.log(parseInt("10", 2));
console.log(parseInt("0x1f"));
console.log(parseInt(" -17.9deg"));
console.log(parseInt("x1f", 16));
console.log(parseInt(".5"), parseFloat(".5"));Example explained
Line 1parseInt('ff', 16) is 255 and parseInt('10', 2) is 2: the same text means different numbers in different bases.
Line 2With no radix, parseInt still special-cases a leading 0x, so parseInt('0x1f') is 31 rather than 0.
Line 3parseInt(' -17.9deg') skips whitespace, accepts the sign, then stops at the first non-digit, returning -17 with no complaint.
Line 4parseInt('.5') is NaN because it needs a digit before the scan can succeed, while parseFloat accepts a leading decimal point and returns 0.5.
Values that convert to 0 instead of failing
Demonstrates the coercion holes in Number and why the global isNaN is the wrong check.
const values = ["", " ", null, undefined, [], ["7"], true, "7,5"];
for (const v of values) {
console.log(`Number(${JSON.stringify(v)}) = ${Number(v)}`);
}
console.log(isNaN("7,5"), Number.isNaN("7,5"));Example explained
Line 1The empty and whitespace-only strings are defined to convert to 0, so an untouched input box looks like the number zero.
Line 2Number(null) is 0 but Number(undefined) is NaN, so whether a missing field fails depends on how it went missing.
Line 3Number(['7']) is 7 because the array is converted to the string '7' first, which is why non-string input deserves its own rejection.
Line 4isNaN('7,5') is true only because the argument is converted first; Number.isNaN says false, since a string is never the NaN value itself.
Gate the input with an explicit format
Validates against a format you chose before converting, and reports failure as null.
const NUMERIC = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)$/;
function parseAmount(raw) {
const text = String(raw).trim();
return NUMERIC.test(text) ? Number(text) : null;
}
for (const raw of ["19.99", "+7", ".5", "12 ", "1_000", "1e3", "0x10"]) {
console.log(`${JSON.stringify(raw)} -> ${parseAmount(raw)}`);
}Example explained
Line 1The ^ and $ anchors make the pattern describe the whole string, so trailing text such as 'px' can never slip through.
Line 2'12 ' passes because trim runs before the test, which is what copy-pasted values need.
Line 3'0x10' is rejected even though Number('0x10') is 16, because an amount field has no reason to accept hexadecimal.
Line 4Returning null rather than NaN gives a failure value that later arithmetic cannot silently absorb.
Important notes
parseInt does not understand exponent notation: parseInt('1e3', 10) is 1 because the scan stops at 'e', while Number('1e3') and parseFloat('1e3') are both 1000.
Only '.' counts as a decimal separator, so locale-formatted text like '1.234,56' must be unformatted first; otherwise Number gives NaN and parseFloat gives 1.234.
Common mistakes
Reading a price with parseInt: parseInt('3.99', 10) is 3, so the cents vanish and the total is wrong with no error raised anywhere.
Validating with if (!n) after converting: 0 is falsy, so a valid input of '0' is treated as a parse failure and overwritten by the default.
Calling parseInt without a radix on data that may start with 0x: parseInt('0x1f') is 31 instead of 0, so a code or id field quietly becomes a different number.
Try it yourself
Change, predict, then run
In a browser console, write parseMinutes(raw) that returns a number for '90', ' 12.5 ' and '0' but null for '', '90min' and '1e2'. Log the result for all six inputs and confirm that the '0' case is not reported as a failure.
Open the JavaScript workspaceCheck your understanding
A quantity field receives the string '12abc'. Which conversion detects the problem instead of handing you a number?
- parseInt('12abc', 10), because the radix forces strict decimal parsing
- Number('12abc'), because the entire string must match the numeric literal grammar
- parseFloat('12abc'), because it keeps scanning until it finds an invalid float
- Both parseInt and Number return NaN here, so either one works
Show answer
Number applies the numeric literal grammar to the whole string, so any leftover character makes the result NaN. parseInt('12abc', 10) still returns 12: the radix only chooses the digit base, it does not make the scan strict, and parseFloat behaves the same way by stopping at 'a'.