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

ConceitoO 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);
});

forEach não suporta break nem continue. Quando precisar de saída antecipada, use for...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.