JavaScript's Array(n).map() Trap: The Sparse Array That Skips Itself

2026-05-23

This function is supposed to build a small lookup table — an array of n incrementing numbers. The author has written it twice, once "clean" and once with what looks like a redundant .fill(). They expect both to behave identically.

// Both should return [0, 1, 2, 3, 4]
function range(n) {
  return Array(n).map((_, i) => i);
}

function rangeFill(n) {
  return Array(n).fill().map((_, i) => i);
}

console.log(range(5));
// => [ <5 empty items> ]    ← what?!

console.log(rangeFill(5));
// => [ 0, 1, 2, 3, 4 ]       ← fine

// Downstream code blows up in confusing ways:
console.log(range(5).length);              // 5  (so it has length!)
console.log(range(5)[2]);                  // undefined
console.log(range(5).reduce((a,b)=>a+b,0)); // 0  (reduce skipped them too)
console.log(JSON.stringify(range(5)));     // "[null,null,null,null,null]"

The Bug

Array(5) does not create an array of five undefined elements. It creates a sparse array with length === 5 but zero own indexed properties. The slots at indices 0–4 are holes, not values.

And here is the trap: most array iteration methods — map, forEach, filter, reduce, some, everyskip holes entirely. They don't visit the index, don't call the callback, and (for map) preserve the hole in the output. So Array(5).map(fn) returns a sparse array of length 5 with fn never having been called.

What makes this especially nasty:

fill() is one of the few methods that operates on holes: it writes undefined into every slot, converting the sparse array into a dense one. After that, map sees real properties and visits each index.

The Fix

Use a constructor that produces a dense array from the start:

// Preferred — explicit and intent-revealing:
function range(n) {
  return Array.from({ length: n }, (_, i) => i);
}

// Also works:
function range(n) {
  return Array(n).fill().map((_, i) => i);
}

// Or:
function range(n) {
  return [...Array(n)].map((_, i) => i);
}

Array.from with a mapping function is the cleanest because it builds the dense array and applies the transform in one pass — no intermediate undefined-filled array, no spread allocation.

The deeper lesson: in JavaScript, "an array of length n" and "an array with n elements" are different things. The language exposes the distinction only through which methods bother to visit which indices, and the rules aren't uniform — for...of visits holes as undefined, classic for (let i=0; i<arr.length; i++) visits them too, but the functional map/forEach/filter family does not. When the iteration silently does nothing, the values that show up downstream are undefined — indistinguishable from a callback bug.

Key Takeaway: Array(n) creates holes, not undefineds, and .map/.forEach/.filter skip holes — so Array(n).map(fn) never calls fn; reach for Array.from({length: n}, fn) instead.

All newsletters