#Arrays#Polyfill

Array.prototype.reduce Polyfill

Implement Array.prototype.reduce from scratch, including the tricky no-initial-value path and the empty-array error case.

By Pratik RaiMedium

Array.prototype.reduce folds an array into a single value by threading an accumulator through a callback. It is the most powerful of the array methods and the one with the most specification detail packed into it — most of which lives in a single question: what happens when you do not pass an initial value.

Why implement it yourself

map and filter are variations on a loop. reduce has genuine branching logic in its setup, a four-parameter callback, and a documented error case. It is also the method people most often use incorrectly, because the no-initial-value form behaves in a way that is not obvious until you have been bitten by it.

The initial value problem

reduce takes an optional second argument. That option changes three things at once:

With an initial value, the accumulator starts as that value and iteration begins at index 0. The callback runs once per element.

Without one, the accumulator starts as the first present element, and iteration begins after it. The callback runs one fewer time.

JSfile.javascript
1[1, 2, 3].reduce((a, b) => a + b); // 6 — callback runs twice 2[1, 2, 3].reduce((a, b) => a + b, 0); // 6 — callback runs three times 3[1, 2, 3].reduce((a, b) => a + b, 10); // 16

And on an empty array with no initial value, it throws. This is the case that reaches production:

JSfile.javascript
1[].reduce((a, b) => a + b); // TypeError: Reduce of empty array with no initial value 2[].reduce((a, b) => a + b, 0); // 0

Any reduce over data whose length you do not control needs an initial value. A filtered list that happens to be empty is enough to crash the page.

Note the phrase "first present element". On a sparse array the seed is the first index that actually exists, not index 0.

The implementation

JSfile.javascript
1Array.prototype.myReduce = function (callback, ...initialValue) { 2 if (this === null || this === undefined) { 3 throw new TypeError('Array.prototype.myReduce called on null or undefined'); 4 } 5 if (typeof callback !== 'function') { 6 throw new TypeError(`${callback} is not a function`); 7 } 8 9 const object = Object(this); 10 const length = object.length >>> 0; 11 12 let index = 0; 13 let accumulator; 14 15 if (initialValue.length > 0) { 16 accumulator = initialValue[0]; 17 } else { 18 // Seed from the first element that is actually present. 19 while (index < length && !(index in object)) index++; 20 if (index >= length) { 21 throw new TypeError('Reduce of empty array with no initial value'); 22 } 23 accumulator = object[index++]; 24 } 25 26 for (; index < length; index++) { 27 if (index in object) { 28 accumulator = callback(accumulator, object[index], index, object); 29 } 30 } 31 32 return accumulator; 33};

Collecting the initial value as a rest parameter is deliberate. Checking arguments.length > 1 works too, but a default parameter would be wrong — reduce(fn, undefined) explicitly passes undefined as the seed, and that must be distinguishable from passing nothing at all.

Also note reduce takes no thisArg. The fourth callback parameter is the array; there is no fifth slot for a context. That asymmetry with map and filter is a favourite interview detail.

Edge cases worth knowing

Forgetting to return the accumulator. The most common reduce bug by a wide margin:

JSfile.javascript
1items.reduce((acc, item) => { acc[item.id] = item; }, {}); // undefined 2items.reduce((acc, item) => { acc[item.id] = item; return acc; }, {}); // correct

The braced arrow returns undefined, which becomes the next accumulator, and the whole thing collapses on the second iteration.

Spreading inside the reducer is quadratic. {...acc, [k]: v} copies the entire accumulator on every element. Over a thousand items that is half a million property copies. Mutating a locally-owned accumulator and returning it is O(n) and perfectly safe — the object never escapes the reduce.

reduceRight is not reduce().reverse(). It walks right to left, which matters for anything non-commutative like function composition or string building.

Common follow-ups

"Implement map with reduce." Straightforward, and a good demonstration that reduce is the general case:

JSfile.javascript
1const map = (arr, fn) => arr.reduce((acc, v, i) => (acc.push(fn(v, i, arr)), acc), []);

"Implement pipe or compose." The canonical use of reduce on an array of functions rather than data — usually the point where the concept clicks.

"When would you not use reduce?" A fair question with a real answer: when a plain loop reads better. A reduce that builds an object with a mutated accumulator is often less clear than a for...of doing the same thing. Reach for it when you are genuinely folding, not to avoid writing a loop.

Key takeaways

  • Without an initial value, the accumulator seeds from the first present element and the callback runs one fewer time.
  • Empty array plus no initial value throws — always seed when the length is not yours to guarantee.
  • Detect the initial value by argument count, never with a default parameter.
  • Return the accumulator on every path.
  • reduce has no thisArg, unlike map and filter.

Goal: Implement myReduce with correct handling of the missing-initialValue path and the empty-array error.

Frequently asked questions

What makes reduce harder to implement than map or filter?
The initial value. When one is supplied the accumulation starts there and the traversal begins at index 0. When it is omitted, the first element of the array becomes the accumulator and the traversal begins at index 1 instead. Getting that branch right is most of the exercise.
What happens on an empty array?
With an initial value, `reduce` returns it untouched and never calls the callback. Without one it throws `TypeError: Reduce of empty array with no initial value`, because there is nothing to start from. Interviewers ask because reproducing the throw shows you read the specification rather than guessing at the happy path.
What arguments does the callback receive?
Four: `(accumulator, currentValue, currentIndex, array)`. Most implementations stop at two and pass the tests anyway, which is exactly why the omission gets probed — the index and the source array are part of the contract even if your test cases never look at them.
What are the usual follow-ups?
Implementing `map` and `filter` in terms of `reduce`, and being asked whether `reduce` should skip holes in sparse arrays. It should, using the same `i in this` check the other iteration methods need.

Related Challenges

Continue learning with these related challenges

View All
JavaScript

Array.prototype.map Polyfill

Implement Array.prototype.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.

JavaScript · Arrays · PolyfillsPratik Rai ·

JavaScript

Array.prototype.filter Polyfill

Implement Array.prototype.filter from scratch. The key detail: push the original element, not the boolean result of the predicate.

JavaScript · Arrays · PolyfillsPratik Rai ·

JavaScript

Array.prototype.groupBy

Group an array into buckets by a key function — now a real language feature as Object.groupBy.

JavaScript · ES6Pratik Rai ·