How to convert a string to a number in JavaScript
Converting a string to a number in JavaScript: an empty string is 0 with Number and NaN with parseInt. What four methods do to awkward input.
Four ways, and they disagree more than you would expect:
Number("42"); // 42
parseInt("42"); // 42
parseFloat("42"); // 42
+"42"; // 42
Identical on a clean input. Here is the same four applied to everything that is not clean, measured in Node v24.14.1.
The comparison table
| Input | Number() |
parseInt() |
parseFloat() |
+ |
|---|---|---|---|---|
"42" |
42 | 42 | 42 | 42 |
"3.14" |
3.14 | 3 | 3.14 | 3.14 |
"12px" |
NaN | 12 | 12 | NaN |
" 7 " |
7 | 7 | 7 | 7 |
"" |
0 | NaN | NaN | 0 |
" " |
0 | NaN | NaN | 0 |
"abc" |
NaN | NaN | NaN | NaN |
"0x1F" |
31 | 31 | 0 | 31 |
"1e3" |
1000 | 1 | 1000 | 1000 |
"Infinity" |
Infinity | NaN | Infinity | Infinity |
"1,000" |
NaN | 1 | 1 | NaN |
Four rows deserve attention.
The empty string is 0. Number("") and +"" both return zero, not NaN. So does a string of only spaces. This is the single most dangerous row in the table, because an empty form field silently becomes a valid number instead of an obvious failure.
parseInt stops at the first character it cannot use. "12px" becomes 12, "3.14" becomes 3 because the decimal point is not part of an integer, and "1,000" becomes 1 because the comma stops it. Forgiving when you want that, dangerous when you do not.
parseFloat("0x1F") is 0. It reads the leading zero and stops at the x. Number and parseInt both understand hexadecimal here and give 31.
parseInt("1e3") is 1. Scientific notation is not integer syntax, so it stops at the e.
The rule that follows from the table
Use Number() when the whole string should be a number, and parseInt when you are extracting a number from the front of something else.
Reading a form field, a JSON value, or an API response? Number(). If the input is not entirely a number you want to know, and NaN tells you.
Reading "12px" from a computed style? parseInt. Partial parsing is the feature.
The unary + is Number() with less typing. Some codebases ban it as too easy to miss when reading; the behaviour is identical.
The empty-string trap, and how to handle it
Number(""); // 0
Number(" "); // 0
Number(null); // 0
Three inputs that mean "no value" and all convert to a real, usable zero.
That matters for form input. A user who leaves a quantity field blank produces "", and Number("") gives 0, so your code proceeds with a valid-looking quantity of zero rather than rejecting the input.
Validate before converting rather than after:
const toNumber = (value) => {
if (typeof value !== "string" || value.trim() === "") return null;
const n = Number(value);
return Number.isNaN(n) ? null : n;
};
That returns null for anything that is not a real number, which is a value you can check for. Returning NaN instead is also reasonable, as long as you check it with Number.isNaN and not === NaN, which is always false.
For comparison, undefined behaves differently from the empty string:
Number(undefined); // NaN
Number(null); // 0
null is zero and undefined is NaN. There is a reason in the specification and no way to make it intuitive; it is worth knowing simply because the two are so often interchangeable elsewhere.
The parseInt trap that catches everyone once
parseInt(0.0000005); // 5
parseInt("0.0000005"); // 0
Same value, different types, wildly different answers.
parseInt takes a string. Given a number, it converts it to one first, and:
String(0.0000005); // "5e-7"
Small numbers stringify to scientific notation. parseInt then reads "5e-7", takes the leading 5, and stops at the e.
The lesson is broader than this one value: parseInt is for strings. Passing it a number is always a mistake, even when it happens to work, because the behaviour depends on how that number stringifies.
The radix argument
parseInt("ff", 16); // 255
parseInt("08", 8); // 0
parseInt("08"); // 8
The second argument is the base. Without it, parseInt infers: a leading 0x means hexadecimal, everything else is decimal.
The parseInt("08", 8) result is worth pausing on. In base 8 the digits are 0 to 7, so 8 is not a valid digit, parsing stops immediately, and you get 0 from the leading zero.
Older advice insists on always passing a radix, because in very old engines a leading zero meant octal, so parseInt("08") gave 0. That was fixed long ago and modern engines return 8. Passing 10 explicitly is still a reasonable habit, and it is no longer the correctness issue it once was.
Precision, and the numbers you cannot convert
Number.MAX_SAFE_INTEGER; // 9007199254740991
Number("9007199254740993"); // 9007199254740992
The converted value is wrong by one. JavaScript numbers are 64-bit floats, so integers above about 9 quadrillion cannot all be represented, and conversion silently returns the nearest one that can.
This bites on database IDs, Twitter-style snowflake IDs, and anything else that ships large integers as strings. If a value arrives as a string and might be large, BigInt is the conversion you want:
BigInt("9007199254740993"); // 9007199254740993n
The fact that such IDs are sent as strings in the first place is usually deliberate, and converting them with Number() undoes the protection.
Converting other things
Number(true); // 1
Number([]); // 0
Number([5]); // 5
Number([1, 2]); // NaN
The array rows follow one rule: an array converts to a string first, then that string converts to a number. [] becomes "" which is 0; [5] becomes "5" which is 5; [1,2] becomes "1,2" which is NaN.
Nobody writes Number([]) deliberately. It happens when a value that was supposed to be a number turns out to be an array, and the result being 0 rather than an error is why the bug surfaces somewhere else entirely.
Validating input properly
Converting and validating are different jobs, and doing them in one step is where most of the bugs come from.
function parseQuantity(input) {
if (typeof input !== "string") return { ok: false, error: "not a string" };
const trimmed = input.trim();
if (trimmed === "") return { ok: false, error: "required" };
const n = Number(trimmed);
if (Number.isNaN(n)) return { ok: false, error: "not a number" };
if (!Number.isFinite(n)) return { ok: false, error: "out of range" };
if (!Number.isInteger(n)) return { ok: false, error: "must be whole" };
if (n < 1) return { ok: false, error: "must be at least 1" };
return { ok: true, value: n };
}
Each check answers one question and returns a message a user can act on. Collapsing them into a single truthiness test gives you "invalid" for six different problems.
Three built-ins do most of the work:
Number.isNaN(n) // is it NaN, without coercing
Number.isFinite(n) // rules out NaN AND Infinity
Number.isInteger(n) // no fractional part
Use the Number. versions rather than the bare globals. isNaN("abc") is true because it coerces the string first, which makes it true for things that are not NaN at all. Number.isNaN only reports actual NaN values.
Number inputs still give you strings
Worth knowing because it surprises people who thought the browser handled it:
<input type="number" id="qty">
document.querySelector("#qty").value; // "5", a string
The value property is always a string, whatever the input's type. type="number" restricts what can be typed and adds spinner controls; it does not change the type of the value you read.
There is a valueAsNumber property that returns a real number, and NaN when the field is empty or invalid:
input.valueAsNumber; // 5, or NaN
That is often the cleaner route, precisely because an empty field gives NaN rather than the 0 that Number("") would produce.
Currency and formatted input
User-entered numbers arrive with formatting, and none of the four methods handles it:
Number("1,000"); // NaN
Number("£10.50"); // NaN
Number("50%"); // NaN
Stripping the characters is the usual approach, and it is locale-dependent in a way that catches people:
Number("1,000".replace(/,/g, "")); // 1000
That works for English formatting and breaks for locales where the comma is the decimal separator, so "1,5" means one and a half. If your input can come from other locales, strip based on the locale's actual separators rather than assuming.
Where you control the interface, an input that rejects formatting is easier than one that parses it, and Intl.NumberFormat handles the display side on the way out.
Three common mistakes
Assuming an empty string fails to convert. Number("") is 0, so a blank form field becomes a valid zero. Check for empty before converting.
Passing a number to parseInt. It stringifies first, and small numbers become scientific notation. parseInt(0.0000005) is 5.
Comparing against NaN with ===. NaN === NaN is false, always. Use Number.isNaN(value), and note that the older global isNaN coerces its argument first, so isNaN("abc") is true for a different reason than you think.
The + operator converts too, and that is the problem
"5" + 1; // "51"
"5" - 1; // 4
+ is overloaded: with a string on either side it concatenates. Every other arithmetic operator converts to a number first, so -, * and / behave numerically on strings while + does not.
That asymmetry is why a value read from an input and added to a total silently produces a string, while the same value subtracted works fine. It is also why the bug surfaces late: total - discount is correct and total + shipping quietly becomes "1050".
The fix is to convert once, at the boundary where the string enters your code, rather than relying on operators to do it where it happens to be used.
Quick reference
Number(str) // whole string must be a number, "" is 0
+str // identical to Number()
parseInt(str, 10) // read an integer from the front, stops at junk
parseFloat(str) // read a decimal from the front
BigInt(str) // for integers beyond MAX_SAFE_INTEGER
Number.isNaN(n) // the correct NaN check
Number.isFinite(n) // rules out NaN and Infinity
Want to run the whole table yourself? Start with the JavaScript track, or read rounding in JavaScript for what happens to these numbers next.
More from the blog
range() in Python: start, stop, step, and why it is lazy
range(1000000) uses 24 bytes. A list of a thousand numbers uses 4,028. Here is what range actually is, measured.
Read moreSort an array of objects by any property in JavaScript
To sort an array of objects in JavaScript, sort() changes the original in place. The default order is alphabetical even on numbers, so 100 beats 9.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.