skip to content

What does `['1', '7', '11'].map(parseInt)` return, and what does that result reveal about how a higher-order function calls the callback you hand it?

level: middleimportance: must knowfreq 60%

answer

  1. prediction [1, 7, 11] is wrong
  2. callback gets element, index, array
  3. parseInt's second parameter is radix
  4. radix 1 illegal, radix 2 reads binary
  5. wrap to fix arity

basics

~20 s

It returns [1, NaN, 3]. The callback is invoked with three arguments — element, index and array — so parseInt receives the index as its radix: radix 1 is invalid and radix 2 reads "11" as binary 3. Wrap the callback to control its arity.

solid answer

~40 s

The result is `[1, NaN, 3]`. `map` calls its callback with `(element, index, array)`, and `parseInt(string, radix)` happens to accept a second parameter, so the index arrives as the radix: `parseInt('1', 0)` treats radix 0 as unspecified and gives 1, `parseInt('7', 1)` is an invalid radix and gives `NaN`, and `parseInt('11', 2)` reads binary and gives 3. The general lesson is that when you pass a bare function reference as a callback you silently opt into the higher-order function's full argument contract, including arguments you never intended to receive. The fix is to control arity yourself: `.map((s) => parseInt(s, 10))`, or `.map(Number)` if plain numeric conversion is what you want. Any callback target with optional extra parameters is a candidate for this bug.

code

javascript · 13 lines
javascript
console.log(['1', '7', '11'].map(parseInt)); // [1, NaN, 3]

// what actually runs
console.log(parseInt('1', 0));  // 1   (radix 0 -> unspecified)
console.log(parseInt('7', 1));  // NaN (radix 1 is illegal)
console.log(parseInt('11', 2)); // 3   (binary)

// fixes: pin the arity yourself
console.log(['1', '7', '11'].map((s) => parseInt(s, 10))); // [1, 7, 11]
console.log(['1', '7', '11'].map(Number));                 // [1, 7, 11]

const unary = (fn) => (x) => fn(x);
console.log(['1', '7', '11'].map(unary(parseInt)));        // [1, 7, 11]

go deeper

for a junior

Know that array callbacks receive the index as a second argument, and that passing a bare function reference exposes it. Remember the safe habit: write (s) => parseInt(s, 10) rather than parseInt.

for a middle

Explain the exact three calls that occur and why radix 0, 1 and 2 produce 1, NaN and 3. An interviewer expects you to generalise it to any callback with optional parameters, not to memorise this one example.

for a senior

Show how you would catch this in real code: which inputs hide it, why a wrong number is more dangerous than a NaN, and how you would review point-free callbacks in a codebase for the same arity mismatch.

for a principal

Own the API-design side: a higher-order function's callback signature is a public contract, so adding an argument to it is a breaking change for every point-free caller. Be ready to argue for narrow callback signatures for exactly that reason.

## The output ```js console.log(['1', '7', '11'].map(parseInt)); // [1, NaN, 3] ``` Most people predict `[1, 7, 11]`. The gap between prediction and reality is exactly why the question gets asked. ## Why the callback receives more than you expected When you pass a function to a higher-order function, the *receiver* decides how it will be invoked. `Array.prototype.map` documents its callback as being called with three arguments: the current element, its index, and the array being traversed. Nothing forces a callback to declare all three; JavaScript quietly discards extra arguments when a function declares fewer parameters. That silence is usually convenient — `.map((x) => x * 2)` ignores the index without ceremony — and occasionally lethal, when the callback you passed *does* declare a second parameter that means something entirely different. `parseInt` has the signature `parseInt(string, radix)`. So the three calls that actually happen are: ```js parseInt('1', 0, arr); // 1 radix 0 -> treated as unspecified, so base 10 parseInt('7', 1, arr); // NaN radix 1 is outside the legal range 2..36 parseInt('11', 2, arr); // 3 binary 11 is decimal 3 ``` The third argument (the array) is simply ignored — `parseInt` declares only two parameters. Note the details worth stating out loud: a radix of `0` (or `undefined`) means "guess", which is base 10 for a plain decimal-looking string; legal radices are 2 through 36, so 1 is rejected with `NaN`; and `'11'` in base 2 is a perfectly valid parse producing 3, which is why the last element is a wrong number rather than `NaN`. A silently wrong number is worse than a `NaN`, because nothing downstream flags it. ## The general rule Passing a bare function reference — `map(parseInt)`, `forEach(this.handle)`, `then(console.log)` — is *point-free* style, and it is only safe when the callback's parameter list is compatible with everything the caller will pass. Ask two questions before you do it: 1. How many arguments does this higher-order function supply? 2. Does my callback declare optional parameters that would absorb them with a different meaning? If the answer to (2) is yes, wrap it. The wrapper is the arity adapter: ```js ['1', '7', '11'].map((s) => parseInt(s, 10)); // [1, 7, 11] ['1', '7', '11'].map(Number); // [1, 7, 11] ``` `Number` is safe here because `Number(value)` ignores any extra arguments — it declares one parameter. That difference, not luck, is what makes one point-free callback fine and the other broken. (`Number` and `parseInt` are not interchangeable in general: `Number('12px')` is `NaN` while `parseInt('12px', 10)` is 12, and `Number('')` is 0 while `parseInt('', 10)` is `NaN`. Pick on parsing semantics, not on which one dodges this bug.) If you want a reusable adapter, a tiny higher-order helper does it: ```js const unary = (fn) => (x) => fn(x); ['1', '7', '11'].map(unary(parseInt)); // [1, 7, 11] ``` `unary` is itself a higher-order function: it takes a function and returns a function that forwards exactly one argument. ## The same trap elsewhere This is not a `map` quirk. Any higher-order function that supplies more arguments than the obvious one can trigger it. Array traversal and predicate methods pass `(element, index, array)`. Reduction passes an accumulator first. Callback-style APIs frequently pass an error or a status argument you did not plan for. The failure mode is identical: your callback has an optional parameter with a different meaning, the caller fills it, and you get a plausible wrong answer rather than an exception. ## Diagnosing it in the wild The symptom is a transformation that is right for some inputs and wrong for others — here, index 0 works, so a one-element test array passes and hides the bug entirely. When a callback misbehaves only for later elements, suspect the index. Logging the callback's `arguments` length, or temporarily wrapping the callback in `(...args) => { console.log(args); return original(...args); }`, exposes the real call shape in seconds.

  • Why is `.map(Number)` safe when `.map(parseInt)` is not?
    `Number` declares a single parameter and ignores anything beyond it, so the index and array arrive and are discarded. `parseInt` declares a second parameter, `radix`, which the index happily fills. The rule is about the callback's declared parameters, not about the two functions being similar — and they are not interchangeable: `Number('12px')` is `NaN` while `parseInt('12px', 10)` is 12.
  • How would you write a helper that makes any function safe to pass point-free?
    A tiny higher-order adapter: `const unary = (fn) => (x) => fn(x);`. It takes a function and returns one that forwards exactly one argument, so extra arguments from the caller are dropped before they reach the target. The same idea generalises to `arity(n, fn)` if you need to keep two arguments.
  • Why does the bug survive a quick unit test?
    Because index 0 behaves correctly — `parseInt('1', 0)` is 1 — so a single-element or all-index-0 test passes. The wrong values appear only from index 1 onward, and `parseInt('11', 2)` returns a plausible number rather than `NaN`. Test data with several elements and non-decimal-looking strings exposes it.

saying these in an interview costs you the question

  • Answers [1, 7, 11] and stops
  • Says map passes only the element to the callback
  • Claims the result is NaN for every element
  • Thinks .map(parseInt, 10) sets the radix
  • Calls it a parseInt bug rather than an arity mismatch

context