Array(n).map() Trap: The Sparse Array That Skips Itself2026-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]"
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, every — skip 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:
arr.length still reports 5, so length-based checks pass.arr[2] returns undefined — the same value you'd get reading a missing property — so it looks like the callback "returned undefined" rather than never having run.JSON.stringify serializes holes as null, masking the problem at API boundaries.Array.from do materialize holes as undefined, so [...Array(5)].map(...) works — which is why the bug survives code review when someone "tests the pattern."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.
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.
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.
