forEach esconde a intenção do código. Os outros métodos não.
forEach é genérico demais. Métodos como map, filter e reduce declaram a intenção do código antes do corpo, facilitando a leitura e manutenção. Saiba quando usar cada um.

forEach não é errado. É genérico demais.
Quando alguém lê um loop forEach, precisa ler o corpo inteiro antes de entender o que ele faz. Transforma dados? Filtra resultados? Procura um item? Valida uma condição? Impossível saber pelo nome do método.
map, filter, find, some, every, reduce — cada um declara a intenção antes do corpo. O nome do método já é documentação.
Esse é o argumento real do functional programming em JavaScript. Não performance, não elegância. Clareza de intenção.
map() — transformação sem modificar o original
forEach com push é o padrão mais comum pra transformar arrays. Funciona. Mas mistura a iteração com a acumulação, e o leitor precisa percorrer tudo antes de entender que o resultado é uma lista transformada.
// ❌ intenção invisível até a última linhaconst nomes = [];usuarios.forEach(usuario => { if (usuario.ativo) { nomes.push(usuario.nome.toUpperCase()); }});// ✅ intenção declarada pelo nome do métodoconst nomes = usuarios .filter(usuario => usuario.ativo) .map(usuario => usuario.nome.toUpperCase());A versão com filter + map diz o que faz antes de mostrar como faz. Cada método tem uma responsabilidade: filter seleciona, map transforma. Separados, cada um é trivial de testar.
Um detalhe importante: map sempre retorna um array do mesmo tamanho. Se você quer tanto filtrar quanto transformar, filter vem antes.
filter() — seleção com critério explícito
forEach com condicional e push acumula elementos que passam em uma condição. O problema é que a condição de seleção fica misturada com a lógica de acumulação.
// ❌ 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); }});// ✅ o critério de seleção está visível na assinaturaconst transacoesGrandes = transacoes .filter(t => t.tipo === 'credito' && t.valor > 100);filter recebe um predicado — uma função que retorna true ou false. Extrair o predicado como função nomeada torna o código ainda mais legível:
const ehCreditoGrande = t => t.tipo === 'credito' && t.valor > 100;const transacoesGrandes = transacoes.filter(ehCreditoGrande);Agora o critério tem nome, pode ser reutilizado, e pode ser testado isoladamente.
reduce() — uma passada, múltiplas computações
reduce é o método mais versátil do array, e o mais incompreendido. A resistência geralmente vem da sintaxe — o acumulador inicial parece mágica na primeira vez.
O caso de uso mais claro: quando você precisa calcular várias coisas sobre o mesmo array em uma passada só.
// ❌ 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;});// ✅ 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 também serve pra transformar arrays em objetos — algo que map e filter não fazem:
// 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)Uma ressalva honesta: reduce com lógica complexa dentro do callback vira difícil de ler. Se o acumulador começa a acumular mais de três coisas diferentes, vale considerar separar em etapas.
flatMap() — transformação + achatamento
flatMap resolve um problema específico: quando cada item do array produz uma lista, e você quer uma lista única no final.
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 também é útil pra filtrar e transformar ao mesmo tempo — retornar [] do callback é equivalente a remover o item:
// transforma e filtra em uma passada: retorna [] pra excluirconst creditos = transacoes.flatMap(t => t.tipo === 'credito' ? [t.valor] : []);some() e every() — validação com early termination
forEach pra validação exige variáveis de controle externas e percorre o array inteiro mesmo quando a resposta já é conhecida.
// ❌ 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; }});// ✅ 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 para no primeiro false. some para no primeiro true. Esse comportamento — short-circuit evaluation — não é documentação de marketing, é como os métodos funcionam na spec do JavaScript.
A vantagem sobre forEach é direta: você não precisa de variável externa, não precisa de flag, e o método já comunica a intenção. every significa "todos devem passar". some significa "pelo menos um deve passar".
find() e findIndex() — busca com early termination
O padrão de busca com forEach tem um problema clássico: precisa verificar se o item já foi encontrado pra não sobrescrever.
// ❌ continua percorrendo mesmo depois de encontrarlet encontrado = null;usuarios.forEach(u => { if (u.id === idBuscado && !encontrado) { encontrado = u; }});// ✅ para assim que encontraconst usuario = usuarios.find(u => u.id === idBuscado);const indice = usuarios.findIndex(u => u.id === idBuscado);find retorna o item ou undefined. findIndex retorna o índice ou -1. Os dois param na primeira ocorrência.
Um padrão útil: find com fallback via operador || ou ??:
const usuario = usuarios.find(u => u.id === id) ?? { nome: 'Convidado', papel: 'guest' };findLast e findLastIndex existem no ES2023 — buscam do fim pro início. Úteis quando a última ocorrência é mais relevante que a primeira.
Composição de funções — combinando operações
Quando várias transformações precisam ser aplicadas em sequência, function composition torna a intenção legível mesmo antes de ler o código.
// 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);Com isso, um pipeline de processamento fica legível como uma lista de etapas:
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);Cada função do pipeline é testável isoladamente. O pipeline em si descreve o processo em linguagem natural.
Isso não é obrigatório — chaining direto funciona bem em muitos casos. Composição faz mais sentido quando você precisa reutilizar pipelines ou quando o número de etapas cresce.
Quando forEach ainda faz sentido
Nem toda iteração precisa retornar algo. forEach é a escolha certa quando o objetivo é um side effect — atualizar o DOM, enviar eventos, logar, disparar requisições:
// forEach faz sentido aqui: side effect intencionalbotoes.forEach(botao => { botao.addEventListener('click', handleClick);});Usar map nesse caso seria errado — map implica que você vai usar o array retornado, e aqui o retorno seria descartado.
A distinção prática:
Se precisa do resultado da iteração →
map,filter,reduce,find,some,everySe quer apenas o efeito da iteração →
forEach
O que muda na prática
Trocar forEach pelos métodos específicos não é refatoração por elegância. É tornar o código mais escaneável — quem lê vê a intenção antes de processar a implementação.
map diz: vou transformar cada item. filter diz: vou selecionar alguns itens. find diz: vou parar no primeiro que passar. some diz: basta um. every diz: todos devem passar.
Código que declara intenção é código que o próximo desenvolvedor — ou você daqui a seis meses — consegue ler sem precisar simular a execução mentalmente.
Esse é o argumento. O resto é consequência.
Referências


