JavaScript flatMap Keeps undefined: Return [] to Skip Items
Contents
A missing return inside a JavaScript flatMap() callback adds an undefined entry. It does not skip the input item. Return [] for a branch that should emit nothing, and [value] for a branch that should emit one value.
For example, [0, 1, 2].flatMap(value => value > 0 ? [value * 10] : []) produces [10, 20]. If the zero branch instead falls through without returning, the result is [undefined, 10, 20]. That first entry exists; it is not a sparse hole.
Make every callback branch define its output
The ECMAScript flatMap algorithm maps values and flattens returned arrays by one level. Its FlattenIntoArray operation tests whether the mapped result is an array. A non-array result is inserted as an element, including undefined.
When reviewing a conditional callback, follow the branch that rejects an item. Does it return [], return undefined, or reach the end of the function? The last two cases leave one entry. Adding braces to an arrow function also means its body needs an explicit return; a standalone expression in that block is not an implicit result.
The following return shapes were checked by the executable fixture below. Each row represents one callback invocation:
| Callback returns | Contribution to the result | Use or diagnosis |
|---|---|---|
[] | No entries | Explicitly skip this input |
undefined | One undefined | A value, not a skip instruction |
[undefined] | One undefined | Explicitly emit that value |
null, false, 0, or '' | One corresponding value | Falsy values are retained |
[[]] | One empty array | The extra wrapper preserves the inner array |
[[1, 2]] | One array containing 1 and 2 | Flattening stops after the wrapper |
[, undefined, 0] | undefined, then 0 | The returned array’s hole is skipped |
This is an output-shape contract. If you need to preserve an input array as one item, return [inputArray]. If you need its elements in the result, return inputArray directly. Neither operation validates the values it emits.
Reproduce the missing return and verify the fix
Save this as flatmap-output.mjs and run node flatmap-output.mjs. The fixture was executed during article preparation on October 29, 2026, using Node.js 24.13.0 on macOS. It requires no packages or network access.
Assertions check actual values, callback indexes, and property presence. Output labels spell out undefined rather than relying on a JSON rendering of the mixed results.
import assert from 'node:assert/strict';
const missing = [0, 1, 2].flatMap(value => { if (value > 0) return [value * 10];});assert.deepEqual(missing, [undefined, 10, 20]);assert.equal(Object.hasOwn(missing, 0), true);console.log('missing return: [undefined, 10, 20]; own index 0');
const selected = [0, 1, 2].flatMap(value => value > 0 ? [value * 10] : []);assert.deepEqual(selected, [10, 20]);console.log('explicit skip: [10, 20]');
const cases = [ ['empty array', [], []], ['undefined', undefined, [undefined]], ['array of undefined', [undefined], [undefined]], ['null', null, [null]], ['false', false, [false]], ['zero', 0, [0]], ['empty string', '', ['']], ['one nested empty array', [[]], [[]]], ['one nested pair', [[1, 2]], [[1, 2]]], ['sparse returned array', [, undefined, 0], [undefined, 0]],];for (const [label, returned, expected] of cases) { assert.deepEqual([1].flatMap(() => returned), expected); console.log(`${label}: length ${expected.length}`);}
const visited = [];const sparse = [, undefined, 0].flatMap((value, index) => { visited.push(index); return [value];});assert.deepEqual(visited, [1, 2]);assert.deepEqual(sparse, [undefined, 0]);console.log('sparse input: visited 1,2; [undefined, 0]');
const batches = [{ items: [0, false, ''] }, { items: [] }];const oneLevel = batches.flatMap(batch => batch.items);const wrapped = batches.flatMap(batch => [batch.items]);assert.deepEqual(oneLevel, [0, false, '']);assert.deepEqual(wrapped, [[0, false, ''], []]);console.log('batch extraction: 3 values; wrapped extraction: 2 arrays');
const record = { id: 1 };const copied = [record].flatMap(value => [value]);assert.equal(copied[0], record);console.log('object identity retained');console.log('all assertions passed');Expected output:
missing return: [undefined, 10, 20]; own index 0explicit skip: [10, 20]empty array: length 0undefined: length 1array of undefined: length 1null: length 1false: length 1zero: length 1empty string: length 1one nested empty array: length 1one nested pair: length 1sparse returned array: length 2sparse input: visited 1,2; [undefined, 0]batch extraction: 3 values; wrapped extraction: 2 arraysobject identity retainedall assertions passedThe first assertion reproduces the failure before the corrected expression runs. Object.hasOwn(missing, 0) then proves that the first result position exists. The ten return-value cases check that the explicit skip rule does not accidentally discard falsy data or flatten another array level.
Remove an accidental wrapper when extracting batches
The fixture’s batches input contains one populated items array and one empty array. Returning batch.items yields three values: 0, false, and an empty string. Returning [batch.items] yields two array entries, including the empty batch.
If an empty array remains in your result, inspect the callback’s exact return shape before appending another .flat(). An extra flattening pass changes the output shape again. It can hide an unintended wrapper while also flattening arrays that another branch intended to preserve.
For this batch contract, empty lists legitimately contribute nothing. A missing items property is a different data condition: returning its undefined value would emit one entry. Decide whether missing data should be rejected, treated as an empty batch, or preserved before implementing that branch. The example assumes every batch has an array-valued items field.
Distinguish a hole from an explicit undefined
MDN documents the sparse-array boundary: callbacks skip missing source slots, and flattening skips holes in returned arrays. In the fixture, [, undefined, 0] visits indexes 1 and 2; returning [value] preserves their explicit undefined and zero.
These sparse checks assume ordinary arrays without inherited indexed properties. Do not infer that a compacted result preserves source indexes. If the index identifies a record, carry it into the emitted record rather than using the result position as the original index.
Appending filter(Boolean) is a separate selection rule. It would discard useful 0, false, and empty strings from the batch example. If you specifically want to remove nullish data, use the explicit predicate explained in filter(Boolean) and retained falsy values. Repairing an unintended missing return at its source makes the callback’s intended output clearer.
The final identity assertion shows that emitting [record] retains the original object. A new result array does not make that record independent. This fixture covers synchronous callbacks, ordinary arrays, and plain records in the stated Node.js version; it does not test browsers, custom array subclasses, proxies, asynchronous callbacks, or performance. Before changing a callback, assert both the intended result shape and the values that must survive.
References: