Code & Development

Stop using forEach: 8 semantic array methods to make your JavaScript code readable

forEach is too generic. Methods like map, filter, and reduce declare code intent before the body, aiding readability. Learn when to use each one.

Stop using forEach: 8 semantic array methods to make your JavaScript code readable

forEach isn't wrong. It's just too generic.

When someone reads a loop forEach, they need to read the entire body before understanding what it does. Does it transform data? Filter results? Look for an item? Validate a condition? It's impossible to tell from the method name.

map, filter, find, some, every, reduce — each declares its intent before the body. The method name is already documentation.

This is the real argument for functional programming in JavaScript. Not performance, not elegance. Clarity of intent.


map() — transformation without modifying the original

forEach with push is the most common pattern for transforming arrays. It works. But it mixes iteration with accumulation, and the reader needs to go through everything before realizing the result is a transformed list.

JS
// ❌ intenção invisível até a última linhaconst nomes = [];usuarios.forEach(usuario => {  if (usuario.ativo) {    nomes.push(usuario.nome.toUpperCase());  }});
JS
// ✅ intenção declarada pelo nome do métodoconst nomes = usuarios  .filter(usuario => usuario.ativo)  .map(usuario => usuario.nome.toUpperCase());

The version with filter + map says what it does before showing how it does it. Each method has a responsibility: filter selects, map transforms. Separated, each is trivial to test.

An important detail: map always returns an array of the same size. If you want to both filter and transform, filter comes first.


filter() — selection with explicit criteria

forEach with a conditional and push accumulates elements that pass a condition. The problem is that the selection condition gets mixed with the accumulation logic.

JS
// ❌ o critério de seleção está escondido dentro do corpoconst resultado = [];transacoes.forEach(t => {  if (t.tipo === 'credito' && t.valor > 100) {    resultado.push(t);  }});
JS
// ✅ o critério de seleção está visível na assinaturaconst transacoesGrandes = transacoes  .filter(t => t.tipo === 'credito' && t.valor > 100);

filter receives a predicate — a function that returns true or false. Extracting the predicate as a named function makes the code even more readable:

JS
const ehCreditoGrande = t => t.tipo === 'credito' && t.valor > 100;const transacoesGrandes = transacoes.filter(ehCreditoGrande);

Now the criteria has a name, can be reused, and can be tested in isolation.


reduce() — one pass, multiple computations

reduce is the most versatile array method, and the most misunderstood. The resistance usually comes from the syntax — the initial accumulator seems like magic the first time.

The clearest use case: when you need to calculate several things about the same array in a single pass.

JS
// ❌ três loops separados pra três cálculos diferenteslet totalCredito = 0;let totalDebito = 0;let porCategoria = {};transacoes.forEach(t => {  if (t.tipo === 'credito') totalCredito += t.valor;});transacoes.forEach(t => {  if (t.tipo === 'debito') totalDebito += t.valor;});transacoes.forEach(t => {  porCategoria[t.categoria] = (porCategoria[t.categoria] || 0) + t.valor;});
JS
// ✅ um reduce, três cálculos simultâneosconst resumo = transacoes.reduce((acc, t) => {  // acumula por tipo  acc[t.tipo] = (acc[t.tipo] || 0) + t.valor;  // acumula por categoria  acc.porCategoria[t.categoria] =    (acc.porCategoria[t.categoria] || 0) + t.valor;  // saldo corrente  acc.saldo += t.tipo === 'credito' ? t.valor : -t.valor;  return acc;}, { credito: 0, debito: 0, porCategoria: {}, saldo: 0 });

reduce also works for transforming arrays into objects — something that map and filter don't do:

JS
// transforma array em mapa indexado por idconst usuariosPorId = usuarios.reduce((acc, usuario) => {  acc[usuario.id] = usuario;  return acc;}, {});// acesso direto: usuariosPorId[42] em vez de .find(u => u.id === 42)

An honest caveat: reduce with complex logic inside the callback becomes hard to read. If the accumulator starts accumulating more than three different things, it's worth considering breaking it into steps.


flatMap() — transformation + flattening

flatMap solves a specific problem: when each item in the array produces a list, and you want a single list at the end.

JS
const usuarios = [  { nome: 'Ana', habilidades: ['React', 'Node', 'GraphQL'] },  { nome: 'Carlos', habilidades: ['Vue', 'Python'] },  { nome: 'Marina', habilidades: ['Angular', 'TypeScript'] }];// ❌ map retorna array de arraysconst errado = usuarios.map(u => u.habilidades);// [['React', 'Node', 'GraphQL'], ['Vue', 'Python'], ['Angular', 'TypeScript']]// com .flat() depois resolve, mas são duas passadasconst manual = usuarios.map(u => u.habilidades).flat();// ✅ flatMap faz as duas em uma sóconst todasHabilidades = usuarios.flatMap(u => u.habilidades);// ['React', 'Node', 'GraphQL', 'Vue', 'Python', 'Angular', 'TypeScript']

flatMap is also useful for filtering and transforming at the same time — returning [] from the callback is equivalent to removing the item:

JS
// transforma e filtra em uma passada: retorna [] pra excluirconst creditos = transacoes.flatMap(t =>  t.tipo === 'credito' ? [t.valor] : []);

some() and every() — validation with early termination

forEach for validation requires external control variables and traverses the entire array even when the answer is already known.

JS
// ❌ percorre o array inteiro mesmo que o primeiro item já seja inválidolet todosValidos = true;usuarios.forEach(u => {  if (!u.email || !u.email.includes('@')) {    todosValidos = false;  }});
JS
// ✅ para na primeira falha — O(1) no melhor casoconst emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;const todosEmailsValidos = usuarios.every(u => emailRegex.test(u.email));const algumAdmin = usuarios.some(u => u.papel === 'admin');

every stops at the first false. some stops at the first true. This behavior — short-circuit evaluation — isn't marketing documentation, it's how the methods work in the JavaScript spec.

The advantage over forEach is direct: you don't need an external variable, you don't need a flag, and the method already communicates the intent. every means "all must pass". some means "at least one must pass".


find() and findIndex() — search with early termination

The search pattern with forEach has a classic problem: you need to check if the item has already been found to avoid overwriting.

JS
// ❌ continua percorrendo mesmo depois de encontrarlet encontrado = null;usuarios.forEach(u => {  if (u.id === idBuscado && !encontrado) {    encontrado = u;  }});
JS
// ✅ para assim que encontraconst usuario = usuarios.find(u => u.id === idBuscado);const indice = usuarios.findIndex(u => u.id === idBuscado);

find returns the item or undefined. findIndex returns the index or -1. Both stop at the first occurrence.

A useful pattern: find with fallback via the || operator or ??:

JS
const usuario = usuarios.find(u => u.id === id) ?? { nome: 'Convidado', papel: 'guest' };

findLast and findLastIndex exist in ES2023 — they search from the end to the beginning. Useful when the last occurrence is more relevant than the first.


Function composition — combining operations

When several transformations need to be applied in sequence, function composition makes the intent readable even before reading the code.

JS
// utilitário: pipe aplica funções da esquerda pra direitaconst pipe = (...fns) => valor => fns.reduce((acc, fn) => fn(acc), valor);// funções reutilizáveis com assinatura (predicate/transform) => array => resultadoconst filtrar = predicado => arr => arr.filter(predicado);const transformar = fn => arr => arr.map(fn);const ordenar = comparar => arr => [...arr].sort(comparar);const pegar = n => arr => arr.slice(0, n);

With this, a processing pipeline becomes readable as a list of steps:

JS
const processarTransacoes = pipe(  filtrar(t => t.tipo === 'debito'),  ordenar((a, b) => b.valor - a.valor),  pegar(5),  transformar(t => ({ ...t, valorFormatado: `R$ ${t.valor.toFixed(2)}` })));const top5Despesas = processarTransacoes(transacoes);

Each function in the pipeline is testable in isolation. The pipeline itself describes the process in natural language.

This isn't mandatory — direct chaining works well in many cases. Composition makes more sense when you need to reuse pipelines or when the number of steps grows.


When forEach still makes sense

Not every iteration needs to return something. forEach is the right choice when the goal is a side effect — updating the DOM, sending events, logging, triggering requests:

JS
// forEach faz sentido aqui: side effect intencionalbotoes.forEach(botao => {  botao.addEventListener('click', handleClick);});

Using map in this case would be wrong — map implies that you will use the returned array, and here the return would be discarded.

The practical distinction:

  • If you need the result of the iteration → map, filter, reduce, find, some, every

  • If you only want the effect of the iteration → forEach


What changes in practice

Swapping forEach for specific methods isn't refactoring for elegance. It's making the code more scannable — whoever reads it sees the intent before processing the implementation.

map says: I will transform each item.filter says: I will select some items.find says: I will stop at the first one that passes.some says: one is enough.every says: all must pass.

Code that declares intent is code that the next developer — or you in six months — can read without needing to mentally simulate the execution.

That is the argument. The rest is a consequence.


References