JavaScript map(parseInt) Returns NaN: Fix the Callback
['10', '10', '10'].map(parseInt) returns [10, NaN, 2], even though every input is identical. Use values.map(value => parseInt(value, 10)) when you intentionally want decimal integer prefixes. Use values.map(Number) for whole numeric strings, or a validating callback when the field must contain a complete decimal integer.
The callback fix removes the accidental radix. It does not decide which input strings your application should accept. The executable fixture below checks both problems separately.
Why the array index becomes the radix
The ECMAScript map algorithm supplies three callback arguments: the element, its index, and the array. Passing parseInt directly connects that index to its second parameter, radix.
For the three identical strings, the effective calls are:
| Index | Effective call | Result | Reason |
|---|---|---|---|
| 0 | parseInt('10', 0) | 10 | Zero selects radix inference; this string is decimal. |
| 1 | parseInt('10', 1) | NaN | Radix 1 is invalid. |
| 2 | parseInt('10', 2) | 2 | Binary 10 represents decimal 2. |
These results follow the parseInt algorithm. The defect can produce a plausible number as well as NaN; filtering out NaN would leave the incorrect 2 in place.
Wrapping the call as value => parseInt(value, 10) supplies the intended radix on every iteration. Adding 10 as map’s second argument does not do that: map(parseInt, 10) sets the callback’s this argument, while the index still reaches parseInt.
Choose the conversion contract before applying the fix
An explicit radix controls the base, but parseInt still consumes an integer prefix. In the fixture, decimal parsing accepts '12px' as 12 and '1e3' as 1. That is useful only when a prefix is the intended data.
Number conversion instead interprets a complete numeric string. The checked inputs show that Number('12px') is NaN, but Number('') and Number(' ') are zero. It also accepts exponent and hexadecimal notation. Switching to Number alone therefore does not enforce a required decimal integer field.
| Your field’s requirement | Callback | Boundary to verify |
|---|---|---|
| Accept a decimal integer prefix | value => parseInt(value, 10) | Trailing characters may be discarded. |
| Accept JavaScript numeric string forms | Number | Blank strings become zero; fractions and other numeric forms are accepted. |
| Require a complete decimal integer within the safe Number range | decimalInteger below | Reject blanks, suffixes, fractions, exponents, hexadecimal notation, and out-of-range values. |
The last row is an application policy implemented here, not a built-in JavaScript definition of valid input. It permits an optional sign and leading zeros, but rejects surrounding whitespace. If your format permits whitespace, normalize it deliberately before validation and add a test for that rule.
Run the callback and input regression checks
Save this as map-conversion.mjs and run node map-conversion.mjs. This exact fixture was executed during article preparation on macOS with Node.js 24.13.0, on October 12, 2026. It uses no packages.
The assertions inspect the callback arguments, reproduce the direct-callback result, verify both simple fixes, and compare eight inputs against the stricter policy. The validator also checks accepted signs and leading zeros.
import assert from 'node:assert/strict';const source = ['10', '10', '10'];const seen = [];const broken = source.map((value, index, array) => { seen.push([value, index, array === source]); return parseInt(value, index);});assert.deepEqual(broken, source.map(parseInt));assert.deepEqual(broken, [10, NaN, 2]);assert.deepEqual(seen, [['10', 0, true], ['10', 1, true], ['10', 2, true]]);assert.deepEqual(source.map(value => parseInt(value, 10)), [10, 10, 10]);assert.deepEqual(source.map(Number), [10, 10, 10]);console.log('callback indexes become radices: 10, NaN, 2');
function decimalInteger(text) { if (typeof text !== 'string' || !/^[+-]?[0-9]+$/.test(text)) { throw new TypeError('Expected a decimal integer string'); } const value = Number(text); if (!Number.isSafeInteger(value)) { throw new RangeError('Integer outside safe range'); } return value;}
const cases = [ ['12', 12, 12, 12], ['12px', 12, NaN, 'reject'], ['1e3', 1, 1000, 'reject'], ['12.9', 12, 12.9, 'reject'], ['', NaN, 0, 'reject'], [' ', NaN, 0, 'reject'], ['0x10', 0, 16, 'reject'], ['9007199254740993', 9007199254740992, 9007199254740992, 'reject'],];for (const [text, prefix, numeric, strict] of cases) { assert.equal(parseInt(text, 10), prefix); assert.equal(Number(text), numeric); if (strict === 'reject') assert.throws(() => decimalInteger(text)); else assert.equal(decimalInteger(text), strict); console.log(`${JSON.stringify(text)} => prefix=${prefix}; Number=${numeric}; strict=${strict}`);}assert.deepEqual(['0', '-12', '+12', '0012'].map(decimalInteger), [0, -12, 12, 12]);assert.throws(() => decimalInteger(null), TypeError);assert.deepEqual(source, ['10', '10', '10']);console.log('strict mapping and unchanged source: passed');Output in the checked environment:
callback indexes become radices: 10, NaN, 2"12" => prefix=12; Number=12; strict=12"12px" => prefix=12; Number=NaN; strict=reject"1e3" => prefix=1; Number=1000; strict=reject"12.9" => prefix=12; Number=12.9; strict=reject"" => prefix=NaN; Number=0; strict=reject" " => prefix=NaN; Number=0; strict=reject"0x10" => prefix=0; Number=16; strict=reject"9007199254740993" => prefix=9007199254740992; Number=9007199254740992; strict=rejectstrict mapping and unchanged source: passedThe last large integer demonstrates why checking only Number.isInteger would be insufficient for this policy: conversion has already rounded the value. The validator rejects the result with Number.isSafeInteger. If exact values outside that range are required, use a separate string or BigInt contract rather than loosening this check. See Number.isSafeInteger for the representable range.
Apply the policy to the actual incoming values
For a required integer field, pass decimalInteger directly to map. Its implementation reads only its first argument, so the extra callback arguments cannot become conversion options. A rejected item throws; this example stops the mapping rather than silently dropping bad records or substituting zero. Decide separately how your application should display that validation failure.
The fixture uses dense arrays of strings. It does not validate sparse arrays, entire records, locale-specific number formats, or a deployed endpoint. Its source-array assertion covers these callbacks, which do not mutate the source. Browser and older Node runtimes were not executed during preparation.
If your array comes from repeated query parameters, preserve every value before converting the query to an object. Then validate each string. A correct numeric callback cannot recover query values discarded earlier.
Replace the direct parseInt callback, then run the input matrix against your field’s acceptance rules. The regression check should prove both that indexes no longer control the radix and that unwanted strings cannot turn into accepted numbers.
References: