1023 words
5 minutes

JavaScript reduce on Empty Arrays: Choose the Initial Value

2026-10-14
Frontend
JavaScript
/
Node.js
/
Troubleshooting

[].reduce((total, value) => total + value) throws a TypeError. For a numeric sum, pass 0 as the second argument: values.reduce((total, value) => total + value, 0). That gives an empty selection a defined total of zero.

The seed must match the operation. Adding 0 to every failing reduction can silently produce the wrong answer, especially when finding the maximum of negative numbers. First decide what an empty collection means to your caller.

Find the empty collection at the reduction boundary#

A nonempty source can become empty after filtering. The MDN error reference describes this failure pattern. Inspect the values passed to reduce, rather than checking only the original array.

In the fixture below, filtering [2, 4] for values greater than ten produces no matches. The unseeded sum throws; the seeded sum returns zero. If an empty selection means “no matching records” rather than a numeric total, return an explicit empty result or throw a domain error before reducing.

Without a supplied initial value, the ECMAScript reduction algorithm searches for the first present element to use as its accumulator. With no such element, it throws. With exactly one present element, it returns that element without calling the reducer.

That last case matters if you put validation inside the callback: a one-element input can bypass it. Validate the input separately when every value must satisfy a contract.

Verify omitted seeds, undefined, and sparse input#

Save this fixture as reduce-empty.mjs and run node reduce-empty.mjs. It was executed during article preparation on October 14, 2026, on macOS with Node.js 24.13.0. The assertions verify ten result-and-callback cases, the filtered sum, and the maximum policy. They use ordinary arrays with no added indexed prototype properties.

import assert from 'node:assert/strict';
const add = (total, value) => total + value;
const selected = [2, 4].filter(value => value > 10);
assert.throws(() => selected.reduce(add), TypeError);
assert.equal(selected.reduce(add, 0), 0);
console.log('filtered sum:', selected.reduce(add, 0));
function trace(values, ...seed) {
const indices = [];
try {
const result = values.reduce((total, value, index) => {
indices.push(index);
return total + value;
}, ...seed);
return { result, indices };
} catch (error) {
if (!(error instanceof TypeError)) throw error;
return { error: error.name, indices };
}
}
const cases = [
['empty / omitted', [], [], { error: 'TypeError', indices: [] }],
['empty / zero', [], [0], { result: 0, indices: [] }],
['empty / undefined', [], [undefined], { result: undefined, indices: [] }],
['one / omitted', [7], [], { result: 7, indices: [] }],
['two / zero', [7, 3], [0], { result: 10, indices: [0, 1] }],
['holes / omitted', Array(3), [], { error: 'TypeError', indices: [] }],
['holes / zero', Array(3), [0], { result: 0, indices: [] }],
['undefined / omitted', [undefined], [], { result: undefined, indices: [] }],
['sparse / omitted', [, 7, , 3], [], { result: 10, indices: [3] }],
['sparse / zero', [, 7, , 3], [0], { result: 10, indices: [1, 3] }],
];
for (const [label, values, seed, expected] of cases) {
const actual = trace(values, ...seed);
assert.deepEqual(actual, expected);
const result = actual.error ?? String(actual.result);
console.log(`${label}: ${result}; callbacks=[${actual.indices}]`);
}
function maximumOrNull(values) {
if (values.length === 0) return null;
return values.reduce((max, value) => Math.max(max, value));
}
assert.equal([-8, -3].reduce((max, value) => Math.max(max, value), 0), 0);
assert.equal(maximumOrNull([-8, -3]), -3);
assert.equal(maximumOrNull([]), null);
console.log('maximum:', maximumOrNull([-8, -3]));
console.log('empty maximum:', maximumOrNull([]));
console.log('assertions: passed');

Output from that environment:

filtered sum: 0
empty / omitted: TypeError; callbacks=[]
empty / zero: 0; callbacks=[]
empty / undefined: undefined; callbacks=[]
one / omitted: 7; callbacks=[]
two / zero: 10; callbacks=[0,1]
holes / omitted: TypeError; callbacks=[]
holes / zero: 0; callbacks=[]
undefined / omitted: undefined; callbacks=[]
sparse / omitted: 10; callbacks=[3]
sparse / zero: 10; callbacks=[1,3]
maximum: -3
empty maximum: null
assertions: passed

Passing undefined explicitly supplies a seed. It differs from omitting the argument: the empty reduction returns undefined without a callback. The fixture uses a rest parameter to preserve that argument-presence distinction; a wrapper that always calls reduce(callback, seed) always supplies a seed, even when its caller omitted one.

Array(3) has length three but no present indexed elements in this setup. A length check alone therefore cannot establish that an unseeded reduction has an accumulator. [undefined] has a present element, so its unseeded reduction returns that value. For [, 7, , 3], the unseeded callback visits index 3; supplying zero also visits index 1. MDN’s reduce reference documents callback skipping for empty slots.

If a preceding filter unexpectedly removed legitimate values, fix that selection rule too. The nullish-filter example distinguishes missing values from zero and false; a seed only defines the reduction’s starting state.

Choose an empty-result policy before a seed#

These choices are verified by the fixture or the additional checks described below. The numeric examples assume dense arrays of finite numbers; they do not validate external input or address floating-point precision.

OperationSeed or guardEmpty resultReason
Numeric sum00Adding zero leaves a numeric value unchanged
Numeric product11Multiplying by one leaves a numeric value unchanged
Maximum, with absence meaningfulGuard empty input, then reduce without a seednull in this exampleNo measurement is different from a measurement of zero
Reduction requiring at least one recordReject empty input firstDomain errorA default would conceal missing required data

For a maximum, zero introduces a value that may not exist in the input. The fixture checks that reducing [-8, -3] from zero returns zero, while maximumOrNull returns -3. Its empty branch returns null deliberately. Callers must handle that branch rather than treating it as an actual measurement.

maximumOrNull assumes dense finite-number input. It is not a sparse-array validator: a nonempty array of holes passes its length guard and still throws during reduction. If your data can be sparse or contain undefined, normalize and validate it before using this function. Keep the distinction between “absent,” “invalid,” and “zero” in that validation.

The product policy can be checked independently:

import assert from 'node:assert/strict';
const product = values => values.reduce((total, value) => total * value, 1);
assert.equal(product([]), 1);
assert.equal(product([2, 3]), 6);
assert.equal(product([0, 3]), 0);
console.log('product assertions: passed');

This block was also executed in the stated Node environment. Returning one for an empty product is the chosen arithmetic contract; an application that requires at least one factor should reject empty input instead. Choose the result your caller can interpret, then verify empty, singleton, and multi-element inputs with that exact policy.

References:

JavaScript reduce on Empty Arrays: Choose the Initial Value
https://laplusda.com/en/posts/javascript-reduce-empty-array-initial-value/
Author
Zero
Published at
2026-10-14
License
CC BY-NC-SA 4.0