JavaScript's Array.map(parseInt) Radix Trap: The Number Parser That Reads Index as Base

2026-07-05

This function is supposed to take an array of numeric strings from a config file and convert them to integers. It works perfectly in the developer's manual tests. Ship it, and watch the metrics dashboard fill with mystery values.

// Parse a list of numeric strings into integers.
// Used to load rate limits from a config file.
function parseLimits(raw) {
    return raw.map(parseInt);
}

const config = ["1", "2", "3", "10", "11", "15", "20"];
const limits = parseLimits(config);

console.log(limits);
// Developer expects: [1, 2, 3, 10, 11, 15, 20]
// Actually prints:   [1, NaN, NaN, 3, 4, 1, NaN]

// Downstream: rate limiter now allows exactly 1 req/sec on
// endpoint #0, blocks endpoint #1 entirely (NaN comparisons
// are always false), and lets endpoint #5 through at 1/sec.

The Bug

Array.prototype.map invokes its callback with three arguments: (element, index, array). And parseInt accepts two arguments: (string, radix). So map(parseInt) silently becomes parseInt(element, index) — the array index is interpreted as the numeric base.

Walking through the example:

The failure is insidious because some values parse to plausible-looking numbers. If your test suite happens to use ["10", "20", "30"], they'll parse as [10, NaN, NaN] — you'll catch the NaN. But ["100", "50", "25"]? Those become [100, NaN, NaN] too. Try ["1", "10", "100"]: [1, NaN, 4]. The corruption pattern shifts with array content, so a "working" dev test tells you nothing.

parseInt's partial-parse behavior compounds the damage: it consumes as many valid digits as it can and returns silently, so "15" in base 5 becomes 1 with no error. Combined with JavaScript's rule that any comparison involving NaN is false (including NaN === NaN), the corrupted config flows downstream and quietly breaks conditionals you'd never think to audit.

The Fix

Never pass a variadic callback to map. Wrap it, or use Number(), which accepts exactly one argument:

// Option 1: explicit arrow, discard extra args
return raw.map(s => parseInt(s, 10));

// Option 2: Number() ignores extras and rejects partial parses
// ("15abc" → NaN, unlike parseInt("15abc", 10) → 15)
return raw.map(Number);

// Option 3: unary plus, same semantics as Number()
return raw.map(s => +s);

The same trap ambushes forEach, filter, and reduce whenever you pass a bare function reference that quietly accepts more arguments than you realized. A lint rule for unicorn/no-callback-reference (or ESLint's array-callback-return family) will catch it before code review does.

Key Takeaway: map passes three arguments to its callback, so any function whose second parameter changes meaning — like parseInt's radix — will silently produce garbage; always wrap it in an arrow that discards the extras.

All newsletters