intermediateCommon5 min read · Updated Jul 18, 2026

Implement Array.prototype.reduce from Scratch

Implement Array.prototype.reduce in JavaScript, including the no-initial-value rule, the empty-array TypeError, and the arguments.length trick most candidates miss.


The problem

Implement Array.prototype.myReduce(callback, initialValue) matching the built-in — including what happens when initialValue is omitted, when it's explicitly undefined, and when the array is empty.

The loop is trivial; the question lives entirely in the initial-value rules. There are three distinct cases and most candidates conflate two of them:

[1, 2, 3].reduce(fn);            // no initial value → acc starts as 1, loop starts at index 1
[1, 2, 3].reduce(fn, undefined); // initial value IS undefined → acc starts as undefined, index 0
[].reduce(fn);                   // no initial value + empty array → TypeError
[].reduce(fn, 0);                // → 0, callback never runs

Case 2 is the planted trap: you cannot detect "omitted" by checking initialValue === undefined — you must check arguments.length.

Where you see it in production

reduce is the universal fold — every aggregation is a special case of it: summing, groupBy (now Object.groupBy), converting arrays to lookup maps, composing functions in pipe, and running promises in series. Interviewers like it because implementing it proves you understand the accumulator contract you use daily.

Implementation

Array.prototype.myReduce = function (callback, initialValue) {
  if (this == null) {
    throw new TypeError("Array.prototype.myReduce called on null or undefined");
  }
  if (typeof callback !== "function") {
    throw new TypeError(`${callback} is not a function`);
  }
 
  const arr = Object(this);
  const length = arr.length >>> 0; // ToUint32, handles array-likes
 
  // arguments.length — NOT a === undefined check — detects omission.
  // reduce(fn, undefined) legitimately starts with acc = undefined.
  const hasInitial = arguments.length >= 2;
 
  let accumulator = initialValue;
  let index = 0;
 
  if (!hasInitial) {
    // Skip holes while searching for the first real element
    while (index < length && !(index in arr)) {
      index++;
    }
    if (index >= length) {
      throw new TypeError("Reduce of empty array with no initial value");
    }
    accumulator = arr[index++]; // first element becomes acc; loop starts after it
  }
 
  for (; index < length; index++) {
    if (index in arr) { // native reduce skips holes in sparse arrays
      accumulator = callback(accumulator, arr[index], index, arr);
    }
  }
 
  return accumulator;
};

Dry run

[5, 10, 20].myReduce((acc, x) => acc + x);
// hasInitial = false (arguments.length === 1)
// index 0 exists → accumulator = 5, index = 1
// i=1: acc = 5 + 10 = 15
// i=2: acc = 15 + 20 = 35
// → 35  (callback ran TWICE for a 3-element array — a classic quiz point)
 
[5, 10, 20].myReduce((acc, x) => acc + x, 100);
// hasInitial = true → acc = 100, callback runs 3 times → 135

Edge cases interviewers probe

  • reduce(fn, undefined) vs reduce(fn) — the arguments.length trick. If your implementation treats them the same, [].reduce(fn, undefined) incorrectly throws (native returns undefined) and [false, 1].reduce(...)-style falsy-first-element logic goes wrong in subtler variants.
  • Empty array, no initial valueTypeError: Reduce of empty array with no initial value. Your implementation should throw, not return undefined.
  • Sparse arrays[1, , 3].reduce((a, b) => a + b) is 4, not NaN: holes are skipped, not visited as undefined. The index in arr guard reproduces this. (Contrast: [1, undefined, 3] visits the undefined.)
  • Callback signature — four arguments: (accumulator, currentValue, index, array). When there's no initial value, the first callback invocation gets index === 1, not 0.
  • Array mutated during reduction — the spec reads length once up front; elements appended during the loop are not visited. Mentioning this shows you read specs, not blog posts.

Common mistakes

  • if (initialValue === undefined) — the single most common bug in this question, and it's exactly what the interviewer planted.
  • Returning undefined for the empty-no-initial case instead of throwing.
  • Starting the no-initial loop at index 0 (double-counts the first element).
  • Forgetting to pass index and array to the callback — real code (e.g. building index maps) relies on them.
  • Writing it as a standalone reduce(arr, fn) when asked for the prototype method, then mishandling this.

Test yourself

Predict the outputWhat are result and calls?
let calls = 0;
const result = [1, 2, 3].reduce((acc, x) => {
  calls++;
  return acc + x;
});
console.log(result, calls);

reduce vs reduceRight vs a for loop

DirectionWhen
reduceleft → rightfolds, aggregations, pipe
reduceRightright → leftcompose, right-associative folds
for...of + accumulator variableeitherwhen the reducer body has early exits or is getting unreadable

Strong candidates volunteer that reduce has no break — if you need early termination, a loop (or some/every) is the honest tool, and "clever" reduce chains that allocate a new object per iteration ({ ...acc, [k]: v } — O(n²)) are a known performance smell in review.

Follow-up questions

  • "Implement map and filter using your myReduce."map: push fn(x) into the accumulator array; filter: push conditionally. Standard warm-down proving the fold is universal.
  • "Why does the spec skip holes?" — consistency with forEach/map hole semantics predating ES5; also why Array(3).reduce(fn) throws (all holes → no first element).
  • "How would you type this in TypeScript?" — the real signature is overloaded: reduce<T>(fn: (acc: T, v: T, ...) => T): T and reduce<U>(fn: (acc: U, v: T, ...) => U, init: U): U. The two-overload shape is the arguments.length rule expressed in types.
  • "Run async work in a reduce?" — the promise-chain fold: see N async tasks in series.
  • "When would you reject reduce in code review?" — accumulator spreads per iteration, side-effect-only reducers (use forEach), or anything a named loop states more clearly.

  • Strip whitespace without regex catastrophes — and enumerate what counts as whitespace.

  • JSON.parse

    advanced

    Write a small recursive-descent parser — the deepest polyfill question in the set.

  • String Tokenizer

    intermediate

    Split source text into tokens — a warm-up for parsing questions like JSON.parse.

  • Serialize values by hand: undefined, functions, cycles, and all the special cases JSON defines.