Array Method Callback Must Return a Value
ID |
javascript.array_callback_return |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
JavaScript |
Tags |
reliability, suspicious-callback |
Description
Reports inline callbacks passed to Array.prototype methods that require a return value but don’t produce one. The affected methods are those whose semantics depend on each callback invocation returning a value:
-
map,flatMap— collect transformed values. -
filter— keep only elements where the callback is truthy. -
reduce,reduceRight— accumulate the next state. -
find,findIndex,findLast,findLastIndex,some,every— test a predicate. -
sort— compute an ordering integer. -
Array.from2-argument overload — map each value while constructing the array.
Two failure modes are flagged:
-
The callback contains no
returnstatement and never throws. Every call yieldsundefined, silently turningmapinto an array of holes,filterinto an empty array,reduceinto aTypeError, and so on. -
The callback contains a bare
return;(no expression). That path also yieldsundefinedand is almost always a missed early-exit value.
Only inline function literals (arrow functions, function expressions, and their async / generator variants) directly passed as the first argument are inspected. Identifier callbacks (e.g. arr.map(cb)) are out of scope — the rule is intentionally local to avoid resolving foreign function bodies. Methods whose callback is not expected to return (e.g. forEach) are ignored.
Rationale
-
These methods are the workhorses of array transformation in JavaScript. A non-returning callback is silently corrupted data:
mapproduces[undefined, undefined, …],filterfilters everything out,reducethrows a confusingTypeError, andsortbecomes order-unstable. -
The bug usually surfaces far away from the broken callback — typically in a downstream
.length, JSON serialization, or anArray.includesthat returnsfalsefor everything. Catching the missing return at the call site avoids those long-range failures. -
A callback that always throws is allowed because the throw is itself an explicit contract — "this configuration is forbidden". A callback that sometimes returns and sometimes does not is the dangerous case, and the bare
return;form is the most common offender.
// Bad
const doubled = numbers.map(n => {
n * 2; // expression statement, value discarded
});
doubled; // [undefined, undefined, …]
// Bad
const positives = numbers.filter(function (n) {
n > 0; // forgot to return
});
positives.length; // 0
// Bad
const total = numbers.reduce((acc, n) => {
acc + n; // returns undefined → TypeError next iteration
}, 0);
Remediation
Add an explicit return of the value the callback is meant to produce, or rewrite the callback as a concise arrow function whose body is the expression itself.
// Good — concise arrow
const doubled = numbers.map(n => n * 2);
// Good — explicit return
const positives = numbers.filter(function (n) {
return n > 0;
});
// Good — every path returns
const total = numbers.reduce((acc, n) => {
return acc + n;
}, 0);
If the side effect is what you actually want (logging, mutation, etc.), switch to a method that does not require a return — typically forEach or a for…of loop.