Control flow
Controle de fluxo é como o código escolhe qual caminho seguir e quantas vezes
repetir um trecho. A ferramenta certa depende de quantas condições existem, se
elas escolhem um valor ou disparam ações, e se o fluxo pode precisar sair no
meio. Este guia vai da ferramenta mais simples (if) à mais específica, sempre
com um exemplo ruim e um bom lado a lado.
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| guard clause (cláusula de proteção) | if no topo da função que retorna cedo em caso inválido; reduz aninhamento |
| early return (retorno antecipado) | Sair da função assim que o resultado for conhecido, sem else desnecessário |
| ternary (ternário) | cond ? a : b: expressão condicional curta; legível só quando as três partes são curtas |
| switch (selecionar caso) | Comando de despacho por valor; bom para enums e mapeamento explícito |
| lookup table (tabela de busca) | Objeto { chave: valor } que substitui cadeias de if/else ou switch simples |
| short-circuit (curto-circuito) | && retorna o primeiro falsy; ` |
| truthy / falsy (avalia como verdadeiro / como falso) | Valores que avaliam como true ou false em contexto booleano (0, "", null são falsy) |
If e else
É o ponto de partida. Para dois caminhos, if/else resolve. Só que um else
depois de um return é ruído: se o fluxo já saiu no return, o else não
acrescenta nada.
❌ Ruim: else desnecessário após return
function getDiscount(user) {
if (user.isPremium) {
return 0.2;
} else {
return 0.05;
}
}
✅ Bom: early return elimina o else
function getDiscount(user) {
if (user.isPremium) return 0.2;
return 0.05;
}
Aninhamento em cascata
Quando as condições se aninham uma dentro da outra, cada nível empurra a lógica principal mais para dentro. O código vira uma pirâmide deitada, um formato que ganhou o apelido de código em forma de seta (arrow antipattern). A guard clause inverte isso: valide as saídas no topo, uma por uma, e deixe o caminho principal reto no fim.
❌ Ruim: lógica enterrada em múltiplos níveis
function processOrder(order) {
if (order) {
if (order.isActive) {
if (order.items.length > 0) {
if (order.customer) {
return process(order);
}
}
}
}
}
✅ Bom: guard clauses, fluxo principal ao fundo
function processOrder(order) {
if (!order) return;
if (!order.isActive) return;
if (order.items.length === 0) return;
if (!order.customer) return;
const invoice = process(order);
return invoice;
}
Coerção implícita
Uma armadilha comum dentro das condições: o == converte os tipos por baixo dos
panos antes de comparar, e o resultado fica imprevisível. Use sempre ===, que
compara valor e tipo sem conversão.
❌ Ruim: coerção silenciosa
if (value != null) {
/* ... */
} // true para undefined também
if (value == false) {
/* ... */
} // true para 0, "" e null
if (count == "3") {
/* ... */
} // converte tipo sem aviso
✅ Bom: comparação explícita
if (value !== null && value !== undefined) {
/* ... */
}
if (!value) {
/* ... */
}
if (count === 3) {
/* ... */
}
Ternário
Serve para escolher entre dois valores numa atribuição, não para controlar o fluxo. Encadeado, vira um quebra-cabeça de ler.
❌ Ruim: lógica inline ilegível
const label =
score >= 90
? "A"
: score >= 80
? "B"
: score >= 70
? "C"
: score >= 60
? "D"
: "F";
✅ Bom: variáveis nomeadas extraem a intenção
const isA = score >= 90;
const isB = score >= 80;
const isC = score >= 70;
const isD = score >= 60;
// prettier-ignore
const grade = isA ? "A"
: isB ? "B"
: isC ? "C"
: isD ? "D"
: "F";
Lookup table
Quando vários if/else (ou case) só devolvem um valor para cada chave, a lista
de condições é na verdade um catálogo. Troque por um objeto: a chave é a
condição, o valor é o resultado.
❌ Ruim: switch repetitivo mapeando chave → valor
function getStatusLabel(status) {
switch (status) {
case "pending":
return "Pending review";
case "approved":
return "Approved";
case "rejected":
return "Rejected";
case "cancelled":
return "Cancelled";
default:
return "Unknown";
}
}
✅ Bom: lookup table: legível e extensível
const STATUS_LABELS = {
pending: "Pending review",
approved: "Approved",
rejected: "Rejected",
cancelled: "Cancelled",
};
function getStatusLabel(status) {
const label = STATUS_LABELS[status] ?? "Unknown";
return label;
}
Switch
A tabela de busca resolve quando cada caso devolve um valor. Quando cada caso
precisa executar várias ações (fazer algo, não só devolver um valor), o switch
deixa a intenção mais clara do que um if/else encadeado. Termine cada case
com break ou return: sem isso, a execução vaza para o próximo caso (o
fall-through), e esse vazamento acidental é um bug silencioso.
❌ Ruim: if/else encadeado para despacho de ações
// prettier-ignore
function processPaymentEvent(event) {
if (event.type === "payment_success") {
sendReceipt(event.orderId);
updateOrderStatus(event.orderId, "settled");
} else if (event.type === "payment_failed") {
notifyFailure(event.userId);
scheduleRetry(event.orderId);
} else if (event.type === "payment_refunded") {
sendRefundConfirmation(event.userId);
updateOrderStatus(event.orderId, "refunded");
}
}
✅ Bom: switch para despacho de comportamento
function processPaymentEvent(event) {
switch (event.type) {
case "payment_success":
sendReceipt(event.orderId);
updateOrderStatus(event.orderId, "settled");
break;
case "payment_failed":
notifyFailure(event.userId);
scheduleRetry(event.orderId);
break;
case "payment_refunded":
sendRefundConfirmation(event.userId);
updateOrderStatus(event.orderId, "refunded");
break;
}
}
Map
A tabela de busca feita com objeto comum tem limites: a chave é sempre convertida
para texto, e não há método pronto para contar o tamanho ou checar a presença com
segurança. O Map é a estrutura certa quando a chave não é texto, quando os
dados mudam em tempo de execução, ou quando você precisa de has, delete e
size prontos.
❌ Ruim: plain object perde o tipo da chave
const userCache = {};
userCache[user.id] = user; // id number vira string
console.log(userCache[123] === userCache["123"]); // true: coerção silenciosa
const count = Object.keys(userCache).length; // verbose
✅ Bom: Map preserva o tipo da chave e traz métodos prontos
const userCache = new Map();
userCache.set(user.id, user);
userCache.has(user.id);
userCache.get(user.id);
userCache.delete(user.id);
userCache.size;
As ferramentas acima decidem qual caminho seguir. As de baixo resolvem a repetição: quantas vezes percorrer uma coleção.
Parar no primeiro resultado
Antes de escrever um laço na mão, veja se find, some ou every já resolvem.
Esses métodos param assim que acham o que procuravam, sem percorrer o resto. Para
uma busca com saída explícita, for...of com return é direto.
❌ Ruim: forEach com flag força percorrer tudo
function findFirstExpiredProduct(products) {
let expiredProduct = null;
products.forEach((product) => {
if (!expiredProduct && product.isExpired) {
expiredProduct = product; // continua iterando mesmo após encontrar
}
});
return expiredProduct;
}
✅ Bom: for...of para no primeiro que corresponde
function findFirstExpiredProduct(products) {
for (const product of products) {
if (product.isExpired) return product;
}
return null;
}
❌ Ruim: forEach percorre tudo mesmo quando o método declarativo existe
function hasExpiredProduct(products) {
let found = false;
products.forEach((product) => {
if (product.isExpired) found = true;
});
return found;
}
✅ Bom: métodos que já param no primeiro resultado
// para no primeiro match
const expiredProduct = products.find((product) => product.isExpired);
// para no primeiro true
const hasExpiredProduct = products.some((product) => product.isExpired);
// para no primeiro false
const allProductsActive = products.every((product) => product.isActive);
forEach
Quando você só precisa fazer algo com cada item de uma coleção (um efeito
colateral, como notificar ou salvar), forEach basta e é claro: sem índice, sem
variável de controle.
❌ Ruim: for com índice quando o índice nunca é usado
for (let i = 0; i < orders.length; i++) {
notifyCustomer(orders[i]);
}
✅ Bom: forEach para efeitos colaterais por item
orders.forEach((order) => {
notifyCustomer(order);
});
forEachnão suportabreaknemcontinue. Quando precisar de saída antecipada, usefor...of.
for...of
Quando o laço precisa sair no meio ou trabalhar direto com os valores, for...of
é a escolha: sem índice no caminho, com break e continue, e funciona com
qualquer coleção percorrível.
❌ Ruim: for...in em array percorre o protótipo
const prices = [10, 20, 30];
for (const index in prices) {
console.log(prices[index]); // índices como strings, inclui herança do protótipo
}
✅ Bom: for...of para valores diretos
const prices = [10, 20, 30];
for (const price of prices) {
console.log(price);
}
❌ Ruim: iteração de objeto com for...of sem Object.entries
const config = { host: "localhost", port: 5432, database: "app" };
for (const key of config) {
console.log(key); // TypeError: config is not iterable
}
✅ Bom: Object.entries() para objetos
const config = { host: "localhost", port: 5432, database: "app" };
for (const [key, value] of Object.entries(config)) {
console.log(`${key}: ${value}`);
}
while
Quando não há uma coleção pronta e a parada depende de uma condição (não de um
índice ou tamanho), while é a escolha natural. Use do...while quando a
primeira volta precisa acontecer sempre, antes de checar a condição.
❌ Ruim: for simulando condição de parada por estado
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const connection = connectToDatabase();
if (connection.isReady) break; // o índice não tem significado aqui
}
✅ Bom: while para condição de parada por estado
let attempt = 0;
while (attempt < maxAttempts) {
const connection = connectToDatabase();
if (connection.isReady) break;
attempt++;
}
❌ Ruim: while quando a fila deve processar ao menos um item
// verifica antes de executar: se a fila já estiver vazia, nunca executa
while (taskQueue.size > 0) {
const task = taskQueue.dequeue();
executeTask(task);
}
✅ Bom: do...while quando a primeira execução é garantida
// drena a fila: processa pelo menos um item antes de verificar
do {
const task = taskQueue.dequeue();
executeTask(task);
} while (taskQueue.size > 0);
DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.