· 5 min read
Why ['1','7','11'].map(parseInt) returns NaN in JavaScript
['1','7','11'].map(parseInt) gives [1, NaN, 3] because map passes the index as parseInt's radix. Here's why, and the safe ways to convert strings.

Run ['1', '7', '11'].map(parseInt) in JavaScript and you get [1, NaN, 3], not [1, 7, 11]. The reason is that map calls its callback with three arguments (the element, its index and the array), and parseInt accepts a second argument: the radix. So parseInt('7', 1) and parseInt('11', 2) run, and those are very different questions. Pass an arrow function, s => parseInt(s, 10), or use Number instead.
That covers the bug itself. The more useful lesson is that any function with an optional second parameter can break the same way when you pass it to map by name.
Reproduce it
['1', '7', '11'].map(parseInt);
// [1, NaN, 3]
['10', '10', '10', '10'].map(parseInt);
// [10, NaN, 2, 3]
The second example makes the pattern visible. The same string '10' produces four different results because the only thing changing is the index, and the index is being read as a number base.
It is easy to miss in a quick test. An array with a single element works, because the first index is 0. A list of small single-digit strings can look half right. The bug tends to surface when the data grows past a few items.
Why does map(parseInt) return NaN?
Two facts combine here.
First, Array.prototype.map always passes three arguments to the callback: the current value, the current index, and the whole array. Most callbacks only declare the first parameter and silently ignore the rest, so nobody notices.
Second, parseInt(string, radix) takes an optional radix. A radix is the number base used to read the digits: 10 for decimal, 16 for hexadecimal, 2 for binary. Valid values are 2 through 36. A radix of 0 (or undefined) means "work it out yourself", which is decimal unless the string starts with 0x. Anything else outside 2 to 36 returns NaN.
When you write .map(parseInt), each call looks like this:
| Call | Radix means | Result |
|---|---|---|
parseInt('1', 0) | auto-detect, so decimal | 1 |
parseInt('7', 1) | base 1 is invalid | NaN |
parseInt('11', 2) | binary | 3 |
The third argument, the array, is ignored by parseInt, so it does no further damage. In base 2, '11' is one two plus one, which is 3. A string like '7' in base 2 would be NaN too, because 7 is not a valid binary digit.
The fix: wrap it, or use Number
The direct fix is to stop passing parseInt by reference and control the arguments yourself:
['1', '7', '11'].map(s => parseInt(s, 10));
// [1, 7, 11]
Always passing the radix is a good habit anyway. It makes the intent explicit and avoids the 0x auto-detection when you only expect decimal input.
If the strings should be whole numeric values, Number is often the better tool. It takes exactly one argument, so the extra arguments from map are harmless:
['1', '7', '11'].map(Number);
// [1, 7, 11]
parseFloat also works by reference, because it only takes one parameter. Still, wrapping it keeps the code safe if you swap it for something else later.
parseInt vs Number: which should you use?
They are not interchangeable. parseInt reads digits from the start of the string and stops at the first character it cannot use. Number converts the whole string or gives up.
parseInt('12px'); // 12
Number('12px'); // NaN
parseInt('12.5'); // 12
Number('12.5'); // 12.5
parseInt(''); // NaN
Number(''); // 0
Number(' '); // 0
parseInt('1e3'); // 1
Number('1e3'); // 1000
A few rules of thumb follow from that:
- Use
parseInt(s, 10)when the input may carry trailing text you want to drop, such as'12px'from a CSS value. - Use
Number(s)when the whole string must be a valid number and partial matches should fail. - Watch the empty string.
Number('')is0, which can hide missing form input. Check for empty values before converting. - If you want an integer from a decimal string,
Math.trunc(Number(s))is clearer than relying onparseIntstopping at the dot.
There is one more parseInt surprise worth knowing. It converts its argument to a string first, so tiny or huge numbers go through exponent notation:
parseInt(0.0000005); // 5, because String(0.0000005) is '5e-7'
parseInt(1e21); // 1
parseInt is for strings. To round or truncate a number, use Math.trunc, Math.floor or Math.round.
The same bug in your own code
parseInt is the famous case, but the trap is general. Any function with an optional second parameter will receive the index when passed straight to map, forEach or filter:
function formatPrice(value, digits = 2) {
return value.toFixed(digits);
}
[1.5, 2.25, 3].map(formatPrice);
// ['2', '2.3', '3.00']
The default digits = 2 only applies when the argument is undefined. map passes 0, 1 and 2, so each price gets a different precision. Worse, this kind of bug can appear months after the call site was written, when someone adds an optional parameter to a helper that was already used point-free (passed by name with no wrapper).
The rule that avoids all of this: pass a function by name only when it takes exactly one parameter and you control it, or when it is a built-in designed for that, like Number, String or Boolean. Otherwise, write the arrow function. It costs a few characters and pins the arguments down.
The same care applies to other callback-based array methods. Default sort has its own conversion surprise, covered in why JavaScript sort() puts 10 before 9, and async callbacks have theirs in why await inside forEach doesn't wait.
Key takeaways
mappasses the value, the index and the array to its callback, not just the value.parseInttreats the index as a radix, so['1', '7', '11'].map(parseInt)returns[1, NaN, 3].- Use
s => parseInt(s, 10)orNumberto convert an array of strings. parseIntstops at the first invalid character;Numberrejects the whole string. Choose based on which behavior you want.- Avoid passing functions with optional parameters to array methods by name.
FAQ
Is map(Number) safe?
Yes. Number uses only its first argument, so the index and array that map passes are ignored. Remember that empty strings become 0 rather than NaN.
Does Number.parseInt behave differently?
No. Number.parseInt is the same function as the global parseInt, so .map(Number.parseInt) has exactly the same problem.
Why does the first element always parse correctly?
The first index is 0, and a radix of 0 tells parseInt to detect the base itself, which is decimal for ordinary digit strings.