Implement Array.prototype.reduce

A reduce polyfill folds an array into a single value by calling the reducer with an accumulator, the current value, its index and the array. When no initial value is supplied, the first element becomes the accumulator and iteration starts at index 1. An empty array with no initial value throws a TypeError.

Easy to hard array iteration accumulator pattern prototype methods

The question

Asked as "write your own reduce", almost always as a prototype method so that [1,2,3].myReduce(...) works. The scoring is on the details rather than the loop:

  1. Does it work with and without an initial value?
  2. Does it throw a TypeError for an empty array with no seed?
  3. Does the callback receive all four arguments?

The solution

The one genuinely tricky part is detecting whether an initial value was passed at all, since undefined is itself a legitimate seed.

Array.prototype.myReduce = function (callbackFn, initialValue) {
  const length = this.length;
  let index = 0;
  let accumulator;

  if (arguments.length >= 2) {
    accumulator = initialValue;
  } else {
    if (length === 0) {
      throw new TypeError("Reduce of empty array with no initial value");
    }
    // With no seed the first element becomes the accumulator.
    accumulator = this[0];
    index = 1;
  }

  while (index < length) {
    accumulator = callbackFn(accumulator, this[index], index, this);
    index += 1;
  }

  return accumulator;
};

How it works

Use arguments.length, not a falsy check. reduce(fn, undefined) and reduce(fn) mean different things and only the argument count tells them apart.

Without a seed, element 0 becomes the accumulator and the loop starts at index 1, so the first element never reaches the callback. Empty and unseeded throws, because there’s no value to return and no honest way to invent one. All four callback arguments get passed, since reducers routinely want the index and the source array.

The tests it has to pass

This is the part most explanations leave out. "It works" is not a claim you can check by reading. These are the 6 cases Practice Pad runs against your solution, and the implementation above passes all of them.

Easy3 cases · seeded and unseeded folds

folds every value into the supplied starting value

expectEqual([1, 2, 3, 4].myReduce((sum, value) => sum + value, 0), 10);

uses the first value as the seed when no starting value is given

expectEqual(["a", "b", "c"].myReduce((text, value) => text + value), "abc");

combines values from left to right

expectEqual([10, 3, 2].myReduce((left, right) => left - right), 5);
Hard3 cases · errors, indices and accumulator types

throws a TypeError for an empty array with no starting value

let threw = false;
try {
  [].myReduce((sum, value) => sum + value);
} catch (error) {
  threw = error instanceof TypeError;
}
expectEqual(threw, true);

passes the index and the whole array to the callback

const joined = ["a", "b", "c"].myReduce(
  (parts, value, index, array) => parts + value + (index < array.length - 1 ? "-" : ""),
  "",
);
expectEqual(joined, "a-b-c");

allows an accumulator of a different type from the values

const sizes = ["apple", "fig", "kiwi"].myReduce((collected, value) => {
  collected[value] = value.length;
  return collected;
}, {});
expectEqual(sizes, { apple: 5, fig: 3, kiwi: 4 });

Edge cases to watch out for

Testing the initial value for truthiness

if (initialValue) treats 0, "" and false as absent, so [1,2].myReduce(fn, 0) silently uses 1 as the seed. Check arguments.length >= 2 instead.

Starting at index 0 when there’s no initial value

The first element has already become the accumulator. Passing it to the callback again double-counts it, which the third easy test catches with subtraction.

Returning undefined for an empty array

The real method throws a TypeError. Returning undefined looks harmless and hides a bug in the caller.

Only passing two arguments to the callback

Plenty of reducers need the index or the whole array. Omitting them works until someone writes a reducer that joins with separators, which is exactly what the second hard test does.

On holes: the real reduce skips empty slots in a sparse array. Guarding the loop with hasOwnProperty is the fix, and mentioning it unprompted is a good signal even when the tests don’t require it.

Follow up questions

Why does reduce throw on an empty array?

With no elements and no initial value there’s no value to return, and inventing one would be wrong for every reducer. The specification chooses to throw a TypeError rather than return undefined, so the mistake surfaces at the call site instead of propagating.

When should you not use reduce?

When a loop would read better, which is more often than reduce enthusiasts admit. Reducing into an object by spreading the accumulator on every step is also quadratic and a common performance bug. If the reducer needs a comment to explain what the accumulator is, a for loop is usually the clearer code.

What arguments does the reduce callback receive?

Four: the accumulator, the current value, the current index, and the array being reduced. The last two are frequently used, for instance to detect the final element and skip a trailing separator.

Can the accumulator be a different type from the elements?

Yes, and that’s much of what makes reduce useful. Reducing an array of strings into an object of lengths, or an array of numbers into a Map, are both ordinary uses.

Practise this with the tests running

Practice Pad runs these 6 cases against your own solution, in a real editor, on your Mac. Twenty-four questions, and an optional AI panel that explains why a test failed.

Download for macOS Free. Requires macOS 12 or later on Apple Silicon.

See all 24 questions