815 words
4 minutes

JavaScript filter(Boolean) Removes 0: Keep Non-Nullish Values

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

[0, false, '', null, undefined, 2].filter(Boolean) returns [2]. If you meant to remove only missing values, the zero, disabled flag, and empty string disappear too. Use values.filter(value => value !== null && value !== undefined) to retain those values while removing null and undefined.

That predicate defines presence, not validity. It also retains NaN, whitespace strings, and empty objects. Decide whether those belong in your data before adding more conditions.

Why Boolean removes valid values#

The ECMAScript filter algorithm keeps an element when the callback’s result converts to true. Passing Boolean uses each element’s truthiness as the keep rule. It does not check whether the element is nullish.

For ordinary JavaScript values, ToBoolean distinguishes zero from the nonempty string '0'. The following matrix is exercised in the fixture below:

Inputfilter(Boolean)Strict nullish filter
null, undefinedRemoveRemove
0, -0, false, ''RemoveKeep
NaN, 0nRemoveKeep
'0', ' 'KeepKeep
[], {}KeepKeep

Use truthiness deliberately when every falsy value should be discarded. For measurements where zero is meaningful, boolean settings where false is meaningful, or text where an empty string is meaningful, use an explicit presence rule instead. Neither predicate validates a record’s fields or parses a string.

Verify the retained values, not just the count#

Save this as filter-values.mjs and run node filter-values.mjs. This exact fixture was executed during article preparation on macOS with Node.js 24.13.0 on October 13, 2026. It requires no packages or network access.

The assertions compare the retained elements and separately check negative zero and NaN. The labels make the output unambiguous: serializing the entire mixed array as JSON would not preserve these distinctions and would fail on its BigInt value.

import assert from 'node:assert/strict';
const cases = [
['null', null], ['undefined', undefined], ['0', 0], ['-0', -0],
['false', false], ['empty string', ''], ['NaN', NaN], ['0n', 0n],
['string 0', '0'], ['space', ' '], ['empty array', []], ['empty object', {}],
];
const present = value => value !== null && value !== undefined;
const values = cases.map(([, value]) => value);
const truthy = values.filter(Boolean);
const nonNullish = values.filter(present);
assert.deepEqual(truthy, ['0', ' ', values[10], values[11]]);
assert.deepEqual(nonNullish, values.slice(2));
assert.equal(Object.is(nonNullish[1], -0), true);
assert.equal(Number.isNaN(nonNullish[4]), true);
for (const [label, value] of cases) {
console.log(`${label}: Boolean=${Boolean(value)}, present=${present(value)}`);
}
console.log(`counts: truthy=${truthy.length}, non-nullish=${nonNullish.length}`);
const wrongOr = value => value !== null || value !== undefined;
assert.deepEqual(values.filter(wrongOr), values);
assert.deepEqual([0, false, '', 2].filter(value => value), [2]);
console.log('OR predicate: retained all 12 values');
const sparse = [, undefined, 0, false];
const visited = [];
const compacted = sparse.filter((value, index) => {
visited.push(index);
return present(value);
});
assert.deepEqual(visited, [1, 2, 3]);
assert.deepEqual(compacted, [0, false]);
assert.equal(0 in sparse, false);
assert.equal(1 in sparse, true);
assert.equal(0 in compacted, true);
assert.equal(sparse.length, 4);
assert.equal(values.length, 12);
console.log(`sparse: visited=${visited.join(',')}, result=${JSON.stringify(compacted)}`);
console.log('all assertions passed');

Expected output:

null: Boolean=false, present=false
undefined: Boolean=false, present=false
0: Boolean=false, present=true
-0: Boolean=false, present=true
false: Boolean=false, present=true
empty string: Boolean=false, present=true
NaN: Boolean=false, present=true
0n: Boolean=false, present=true
string 0: Boolean=true, present=true
space: Boolean=true, present=true
empty array: Boolean=true, present=true
empty object: Boolean=true, present=true
counts: truthy=4, non-nullish=10
OR predicate: retained all 12 values
sparse: visited=1,2,3, result=[0,false]
all assertions passed

Two predicates that fail the intended rule#

The fixture checks both mistakes:

  • value !== null || value !== undefined retains everything. A null value is not undefined, and an undefined value is not null; at least one comparison is always true. Use && so both exclusions must pass.
  • value => value returns the element as the callback result. Filter converts that result to a boolean, so zero, false, and the empty string still disappear. Return the presence comparison instead.

A block-bodied callback also needs a return statement. If it only evaluates a comparison and returns nothing, its result is undefined. Filter then rejects that element. Keep the expression-bodied arrow from the opening, or explicitly return the comparison.

A sparse hole is not an undefined element#

In [, undefined, 0, false], index 0 has no element. Index 1 exists and holds undefined. With ordinary arrays and no inherited numeric properties, filter skips the hole but calls the callback for the explicit undefined value. MDN documents this sparse-array behavior.

The fixture records callback indexes 1,2,3, removes the undefined element, and produces [0,false] with new indexes 0,1. It also checks that the input still has length 4 and its first slot remains absent. These predicates do not mutate the source, but filtering changes the output positions. If positions correspond to columns or slots, removing missing elements may break that correspondence even when zero and false survive.

The examples cover ordinary arrays, primitive values, two object values, and a sparse array with the standard unmodified prototype. They do not test array subclasses, proxies, callback-driven mutation, browser-only objects such as document.all, or TypeScript’s inferred types. The runtime predicate answers which values remain; it does not establish a compiler type-narrowing guarantee.

For an object field that survives filtering but disappears when you build a request, inspect the separate JSON.stringify undefined-field boundary. Array selection and JSON serialization are different stages with different rules.

Choose the removal rule before shortening the callback. To remove only null and undefined, compare against both explicitly, then assert that the meaningful falsy values are still present.

References:

JavaScript filter(Boolean) Removes 0: Keep Non-Nullish Values
https://laplusda.com/en/posts/javascript-filter-boolean-keep-zero-false/
Author
Zero
Published at
2026-10-13
License
CC BY-NC-SA 4.0