Se você escreve JavaScript há mais de alguns meses, já sentiu a dor de callbacks profundamente aninhados e cadeias .then() embaralhadas. O async/await, introduzido no ES2017, resolveu tudo isso — e mesmo assim os desenvolvedores ainda caem nas mesmas três ou quatro armadilhas. Vamos percorrer tudo isso corretamente: como funciona, como lidar com erros bem, e os padrões de execução paralela que realmente importam para a performance.

O Básico — Funções async e await

Uma função async sempre retorna uma Promise. Dentro dela, await pausa a execução até que a Promise aguardada seja resolvida. Esse é o modelo mental completo:

js
async function fetchUserProfile(userId) {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const profile = await response.json();
  return profile; // wrapped in a Promise automatically
}

// Calling an async function gives you a Promise
const profilePromise = fetchUserProfile(42);
profilePromise.then(profile => console.log(profile.name));

// Or use await at the call site
const profile = await fetchUserProfile(42);
console.log(profile.name);

A palavra-chave await só pode ser usada dentro de uma função async — ou no nível superior de um módulo ES (mais sobre isso depois). Usá-la em qualquer outro lugar é um erro de sintaxe.

Tratamento de Erros — A Forma Certa e a Errada

É aqui que a maioria dos tutoriais se perde. O instinto é envolver tudo em try/catch e achar que terminou. Funciona, mas blocos catch vazios são um sinal de código ruim que mascara bugs reais:

js
// ❌ Don't do this — silent failure, impossible to debug
async function loadConfig() {
  try {
    const res = await fetch('/api/config');
    return await res.json();
  } catch (err) {
    // swallowed — you'll never know what broke
  }
}

// ✅ Do this — handle errors explicitly, return something meaningful
async function loadConfig() {
  try {
    const res = await fetch('/api/config');
    if (!res.ok) {
      throw new Error(`Config fetch failed: ${res.status} ${res.statusText}`);
    }
    return await res.json();
  } catch (err) {
    console.error('loadConfig error:', err.message);
    return null; // caller can check for null
  }
}

Prefiro um padrão onde funções async retornam null em caso de falha ou lançam erros intencionais. O que evito é capturar um erro, registrá-lo em log e depois retornar um valor que faz o chamador pensar que a requisição foi bem-sucedida.

Dica profissional: Se você tiver múltiplos awaits em um bloco try, um único catch lida com todos eles — mas você perde o contexto sobre qual falhou. Para fluxos complexos, considere blocos try/catch separados ou um helper como await to(promise) que retorna tuplas [error, data].

Sequencial vs Paralelo — A Armadilha de Performance

Este é o erro que vejo com mais frequência em código de produção. Quando você faz await em cada chamada uma após a outra, você as executa sequencialmente — mesmo quando são completamente independentes entre si:

js
// ❌ Sequential — takes ~900ms total (300 + 300 + 300)
async function loadDashboard(userId) {
  const user     = await fetchUser(userId);       // 300ms
  const orders   = await fetchOrders(userId);     // 300ms
  const settings = await fetchSettings(userId);  // 300ms
  return { user, orders, settings };
}

// ✅ Parallel with Promise.all — takes ~300ms total
async function loadDashboard(userId) {
  const [user, orders, settings] = await Promise.all([
    fetchUser(userId),
    fetchOrders(userId),
    fetchSettings(userId)
  ]);
  return { user, orders, settings };
}

Promise.all() dispara as três requisições ao mesmo tempo e aguarda a conclusão de todas. Se qualquer uma delas for rejeitada, o conjunto todo é rejeitado. Isso geralmente é o que você quer para carregamento no estilo dashboard onde todos os dados são necessários.

Promise.allSettled — Quando a Falha Parcial É OK

Às vezes você quer disparar múltiplas requisições e usar o que retornar com sucesso, mesmo que algumas falhem. Promise.allSettled() foi criado exatamente para isso:

js
async function loadWidgets(widgetIds) {
  const results = await Promise.allSettled(
    widgetIds.map(id => fetchWidget(id))
  );

  const widgets = [];
  const errors  = [];

  for (const result of results) {
    if (result.status === 'fulfilled') {
      widgets.push(result.value);
    } else {
      errors.push(result.reason.message);
    }
  }

  if (errors.length > 0) {
    console.warn('Some widgets failed to load:', errors);
  }

  return widgets; // return whatever succeeded
}

Esse padrão é ótimo para elementos de UI não críticos — como uma barra lateral com múltiplas seções independentes. Se uma falhar, você mostra as demais em vez de deixar a página em branco.

async em Loops — A Armadilha do forEach

Essa aqui já pegou todo mundo pelo menos uma vez. Array.forEach() não aguarda callbacks async — ele os dispara e imediatamente segue em frente. O loop termina antes que qualquer trabalho async seja concluído:

js
const orderIds = [101, 102, 103, 104];

// ❌ forEach ignores async — all requests fire in parallel uncontrolled,
// and code after the forEach runs before any complete
orderIds.forEach(async (id) => {
  await processOrder(id); // NOT awaited by forEach
});
console.log('done?'); // prints before any order is processed

// ✅ for...of — sequential, fully awaited
for (const id of orderIds) {
  await processOrder(id);
}
console.log('done'); // prints after all orders are processed

// ✅ Parallel but controlled — all fire at once, await all completions
await Promise.all(orderIds.map(id => processOrder(id)));
console.log('done'); // prints after all orders are processed

Use for...of quando a ordem importa ou quando você precisa limitar requisições (processar uma de cada vez). Use Promise.all(map(...)) quando quiser máximo paralelismo e não precisar de garantias sequenciais.

await no Nível Superior em Módulos ES

Desde o ES2022, você pode usar await no nível superior de um módulo ES — sem função wrapper necessária. Isso é muito útil para inicialização de módulos que dependem de dados async:

js
// config.js (ES module)
const response = await fetch('/api/runtime-config');
const config   = await response.json();

export const API_BASE_URL  = config.apiBaseUrl;
export const FEATURE_FLAGS = config.featureFlags;
js
// main.js — imports wait for config.js to fully resolve
import { API_BASE_URL, FEATURE_FLAGS } from './config.js';

console.log(API_BASE_URL); // guaranteed to be loaded

O await no nível superior funciona no Node.js 14.8+ com "type": "module" no package.json, e em todos os navegadores modernos via módulos ES nativos. A execução do módulo importador é pausada até que o módulo aguardado seja totalmente resolvido — que é exatamente a garantia que você precisa.

Um Pipeline Real — Buscar, Analisar e Transformar

Aqui está um pipeline async realista que combina tudo: busca de uma API, tratamento de erros HTTP, transformação dos dados e fallback gracioso em caso de falha:

js
async function getProductCatalog(categoryId) {
  // Step 1: fetch raw data
  const response = await fetch(
    `https://api.shop.example.com/categories/${categoryId}/products`,
    { headers: { Authorization: `Bearer ${getAuthToken()}` } }
  );

  if (!response.ok) {
    throw new Error(`Catalog fetch failed: ${response.status}`);
  }

  // Step 2: parse JSON
  const raw = await response.json();

  // Step 3: transform into the shape your UI needs
  const products = raw.items.map(item => ({
    id:       item.product_id,
    name:     item.display_name,
    price:    (item.price_cents / 100).toFixed(2),
    inStock:  item.inventory_count > 0,
    imageUrl: item.media?.[0]?.url ?? '/images/placeholder.png'
  }));

  // Step 4: filter out anything that's been discontinued
  return products.filter(p => !p.discontinued);
}

// Usage
try {
  const catalog = await getProductCatalog('electronics');
  renderProductGrid(catalog);
} catch (err) {
  showErrorBanner(`Could not load products: ${err.message}`);
}

Ferramentas Úteis

Quando você está depurando código async que processa payloads JSON, estas ferramentas ajudam: Formatador JSON para inspecionar respostas de API, Validador JSON para capturar payloads malformados antes que cheguem ao seu código, e Formatador JS para limpar código de função async. Para a especificação completa de async/await, o guia async do MDN é a referência mais completa, e a especificação TC39 cobre a semântica exata se você precisar.

Conclusão

async/await torna o JavaScript assíncrono legível — mas as armadilhas são reais. Sempre verifique response.ok antes de analisar, nunca deixe blocos catch engolir erros silenciosamente, use Promise.all() para chamadas paralelas independentes, e fique longe de forEach com callbacks async. Incorpore esses hábitos e seu código async será rápido e fácil de depurar.