Control flow
Controle de fluxo evolui com a complexidade. A ferramenta certa depende de quantas condições existem, se mapeiam valores ou executam ações, e se o fluxo pode precisar de saída antecipada. Guard clauses (cláusulas de proteção) achatam aninhamento; lookup tables (tabelas de busca) eliminam cadeias longas de if/else.
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 coercionam para true ou false em contexto booleano (0, "", null são falsy) |
If e else
O ponto de partida. Para dois caminhos, if/else funciona. O else após um
return é ruído: o fluxo já saiu.
❌ 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 crescem e se aninham, cada nível enterra a lógica um nível mais fundo. O fluxo vira uma pirâmide: o arrow antipattern.
Guard clauses invertem: valide as saídas no topo e deixe o fluxo principal limpo.
❌ 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
Trap frequente dentro de condicionais: == coerce tipos silenciosamente e torna
a comparação imprevisível.
❌ 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
Para atribuição de dois valores possíveis, não para lógica de fluxo. Encadeado, vira puzzle (quebra cabeça).
❌ 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 múltiplos guards ou if/else retornam um valor para cada chave, a lista
de condições vira um catálogo. Substitua 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
Lookup table resolve mapeamento de valores. Quando cada caso precisa executar
múltiplas ações (não retornar um valor, mas fazer algo), switch torna a
intenção mais clara que um if/else encadeado. Cada case termina com break
ou return explícito: fall-through acidental é 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, "paid");
} 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, "paid");
break;
case "payment_failed":
notifyFailure(event.userId);
scheduleRetry(event.orderId);
break;
case "payment_refunded":
sendRefundConfirmation(event.userId);
updateOrderStatus(event.orderId, "refunded");
break;
}
}
Map
Lookup table com plain object tem limitações: chaves são sempre coercidas para
string e não há métodos nativos para tamanho ou verificação segura. Map é a
estrutura certa quando a chave não é string, quando os dados são dinâmicos, ou
quando você precisa de has, delete e size nativos.
❌ 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 tipo e tem **API** (Application Programming Interface, Interface de Programação de Aplicações) nativa
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 resolvem decisão: qual caminho seguir. As abaixo resolvem iteração: quantas vezes percorrer.
Circuit break
Antes de escrever um loop, verifique se find, some ou every já resolve.
Esses métodos param no primeiro match, sem percorrer o resto. Para busca com
lógica de 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 sai no primeiro match
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 declarativos com circuit break nativo
// 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
Para efeitos colaterais sobre cada item de uma coleção, forEach é declarativo
e suficiente: 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 de saída antecipada ou iteração com valores diretos,
for...of é a escolha: sem índice implícito, com suporte a break e
continue, compatível com qualquer iterá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á coleção pré-definida e o critério de parada é uma condição, não um
índice ou tamanho, while é a escolha natural. Use do...while quando a
primeira iteração deve sempre executar, independente da 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);
Desenvolvido por @thiagocajadev · Fork baseado no repositório pmndrs/docs · Poimandres.