Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. How to convert a string to a number in JavaScript
Learn

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.

By Max Arthur
Co-Founder & Content Marketer·October 3, 2026·9 Min read
White text reading 'How to convert a string to a number in JavaScript' on a dark purple dotted background

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.

Keep reading

More from the blog

White text reading 'How range() works in Python' on a dark purple dotted background
October 12, 2026·8 min readLearn

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 more
White text reading 'How to sort an array of objects in JavaScript' on a dark purple dotted background
October 2, 2026·9 min readLearn

Sort 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 more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentReact DeveloperPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs Mimo
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs Sololearn
Devpuff vs freeCodeCamp
Read All Comparisons