Promises: resolvendo o Callback Hell

[100] Promises: resolvendo o Callback Hell

Uma Promise é o recibo de uma operação que ainda não terminou: você guarda o papel e segue a vida. O artigo mostra os três estados, a cadeia de .then que substitui a pirâmide de callbacks por linhas na vertical, o catch único que cobre a cadeia inteira, e os quatro combinadores — all, allSettled, race e any.
Javascript

19 min de leitura

No artigo Callbacks: o começo de tudo vimos o Callback Hell — código que cresce horizontalmente, difícil de ler e manter. As Promises foram introduzidas no ES6 (2015) como a solução oficial do JavaScript para esse problema.

Uma Promise representa um valor que ainda não está disponível, mas que estará em algum momento no futuro — ou que pode falhar. Em vez de passar um callback para dentro de uma função, você recebe um objeto que representa a operação em andamento e encadeia o que fazer com o resultado.

O conceito

Pense em uma Promise como um recibo de pedido num restaurante. Você faz o pedido (inicia a operação), recebe um número (a Promise), e continua fazendo outras coisas. Quando o pedido fica pronto, o garçom te avisa (resolve). Se algo deu errado na cozinha, você também é avisado (reject).

// Uma Promise tem sempre um de três estados:
// pending   → aguardando (operação em andamento)
// fulfilled → concluída com sucesso (resolve foi chamado)
// rejected  → falhou (reject foi chamado)

Criando uma Promise

const minhaPromise = new Promise(function(resolve, reject) {
  // O executor — executa imediatamente

  const sucesso = true;

  if (sucesso) {
    resolve("Operação concluída com sucesso!"); // a promise passa a fulfilled
  } else {
    reject(new Error("Algo deu errado.")); // a promise passa a rejected
  }
});

O construtor recebe uma função chamada executor, que recebe dois callbacks:

  • resolve(valor) — chame quando a operação tiver sucesso
  • reject(erro) — chame quando a operação falhar

Consumindo com .then() e .catch()

minhaPromise
  .then(function(resultado) {
    // Executado se a promise foi resolvida
    console.log(resultado); // "Operação concluída com sucesso!"
  })
  .catch(function(erro) {
    // Executado se a promise foi rejeitada
    console.error(erro.message);
  });

Com arrow functions — como você verá na prática:

minhaPromise
  .then(resultado => console.log(resultado))
  .catch(erro => console.error(erro.message));

Promise assíncrona — o caso real

function buscarUsuario(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id <= 0) {
        reject(new Error("ID inválido."));
        return;
      }

      resolve({
        id,
        nome: "Ana Paula",
        email: "ana@email.com",
        plano: "premium",
      });
    }, 1000);
  });
}

// Usando a função
buscarUsuario(42)
  .then(usuario => {
    console.log(`Olá, ${usuario.nome}!`);
    console.log(`Plano: ${usuario.plano}`);
  })
  .catch(erro => {
    console.error(`Erro: ${erro.message}`);
  });

buscarUsuario(-1)
  .then(usuario => console.log(usuario.nome))
  .catch(erro => console.error(`Erro: ${erro.message}`)); // Erro: ID inválido.

Compare com a versão em callback do artigo Callbacks: o começo de tudo — muito mais limpo.

Encadeamento de .then()

A grande vantagem das Promises: você pode encadear operações dependentes de forma linear, sem aninhar:

function buscarUsuario(id) {
  return new Promise(resolve => {
    setTimeout(() => resolve({ id, nome: "Carlos", enderecoId: 7 }), 500);
  });
}

function buscarEndereco(enderecoId) {
  return new Promise(resolve => {
    setTimeout(() => resolve({ id: enderecoId, cidade: "Curitiba", rua: "Av. Brasil" }), 400);
  });
}

function buscarPedidos(usuarioId) {
  return new Promise(resolve => {
    setTimeout(() => resolve([{ id: 101, total: 250 }, { id: 102, total: 180 }]), 600);
  });
}

// Encadeamento linear — sem pirâmide!
buscarUsuario(1)
  .then(usuario => {
    console.log(`Usuário: ${usuario.nome}`);
    return buscarEndereco(usuario.enderecoId); // retorna uma nova Promise
  })
  .then(endereco => {
    console.log(`Cidade: ${endereco.cidade}`);
    return buscarPedidos(1); // retorna uma nova Promise
  })
  .then(pedidos => {
    console.log(`Pedidos: ${pedidos.length}`);
    const total = pedidos.reduce((acc, p) => acc + p.total, 0);
    console.log(`Total: R$ ${total}`);
  })
  .catch(erro => {
    // Um único catch trata erros de QUALQUER etapa da cadeia
    console.error(`Algo deu errado: ${erro.message}`);
  });

Antes eram 4 níveis aninhados de callbacks. Agora é uma cadeia linear e legível. O catch no final captura qualquer erro de qualquer etapa.

Como o encadeamento funciona

Cada .then() retorna uma nova Promise. O valor retornado dentro do .then() se torna o valor resolvido da próxima Promise na cadeia:

Promise.resolve(1)
  .then(valor => {
    console.log(valor); // 1
    return valor + 1;   // retorna 2
  })
  .then(valor => {
    console.log(valor); // 2
    return valor * 3;   // retorna 6
  })
  .then(valor => {
    console.log(valor); // 6
  });

Se você retornar uma Promise dentro do .then(), o encadeamento espera essa Promise resolver antes de chamar o próximo .then().

.finally() — executar sempre

Assim como o finally do try/catch, ele executa independente de sucesso ou falha:

function carregarDados() {
  mostrarLoading(true);

  return buscarUsuario(1)
    .then(usuario => {
      exibirUsuario(usuario);
    })
    .catch(erro => {
      exibirErro(erro.message);
    })
    .finally(() => {
      mostrarLoading(false); // sempre oculta o loading
    });
}

Promise.resolve() e Promise.reject()

Atalhos para criar Promises já resolvidas ou rejeitadas:

// Já resolvida
Promise.resolve("valor imediato")
  .then(v => console.log(v)); // "valor imediato"

// Já rejeitada
Promise.reject(new Error("falha imediata"))
  .catch(e => console.error(e.message)); // "falha imediata"

// Útil para tornar funções síncronas compatíveis com código assíncrono
function obterConfiguracao(chave) {
  const cache = { tema: "escuro", idioma: "pt-BR" };

  if (cache[chave]) {
    return Promise.resolve(cache[chave]); // retorna Promise mesmo sendo síncrono
  }

  return buscarConfiguracaoDoServidor(chave); // assíncrono real
}

Promise.all() — executar em paralelo

Executa múltiplas Promises ao mesmo tempo e aguarda todas terminarem:

const promessas = [
  buscarUsuario(1),
  buscarUsuario(2),
  buscarUsuario(3),
];

Promise.all(promessas)
  .then(usuarios => {
    // usuarios é um array com os resultados na mesma ordem
    console.log(`${usuarios.length} usuários carregados`);
    usuarios.forEach(u => console.log(`- ${u.nome}`));
  })
  .catch(erro => {
    // Se QUALQUER uma falhar, cai aqui
    console.error(`Falha: ${erro.message}`);
  });

Promise.all é essencial para performance — em vez de fazer 3 requisições em sequência (3 segundos), faz as 3 ao mesmo tempo (~1 segundo, o tempo da mais lenta).

Promise.allSettled() — quando quer todos os resultados

Diferente do Promise.all, o allSettled não falha se uma Promise rejeitar — retorna o resultado de todas, sejam sucessos ou falhas:

const promises = [
  buscarUsuario(1),    // vai funcionar
  buscarUsuario(-1),   // vai rejeitar
  buscarUsuario(3),    // vai funcionar
];

Promise.allSettled(promises)
  .then(resultados => {
    resultados.forEach((resultado, i) => {
      if (resultado.status === "fulfilled") {
        console.log(`✅ Usuário ${i + 1}: ${resultado.value.nome}`);
      } else {
        console.log(`❌ Usuário ${i + 1}: ${resultado.reason.message}`);
      }
    });
  });

// ✅ Usuário 1: Ana Paula
// ❌ Usuário 2: ID inválido.
// ✅ Usuário 3: Ana Paula

Promise.race() — o mais rápido vence

Resolve ou rejeita assim que a primeira Promise terminar:

const timeout = new Promise((_, reject) =>
  setTimeout(() => reject(new Error("Timeout: requisição muito lenta.")), 3000)
);

const requisicao = buscarUsuario(1);

Promise.race([requisicao, timeout])
  .then(usuario => console.log(`Carregado: ${usuario.nome}`))
  .catch(erro => console.error(erro.message));
// Se buscarUsuario demorar mais de 3s → "Timeout: requisição muito lenta."
// Se terminar antes → exibe o usuário

Isso é um padrão muito útil para implementar timeout em requisições.

Promise.any() — o primeiro sucesso

Resolve com o primeiro que tiver sucesso. Só rejeita se todos falharem:

// Tentar múltiplos servidores — usa o que responder primeiro
const servidores = [
  buscarDoServidor("servidor-1"),
  buscarDoServidor("servidor-2"),
  buscarDoServidor("servidor-3"),
];

Promise.any(servidores)
  .then(dados => console.log("Dados recebidos:", dados))
  .catch(erro => console.error("Todos os servidores falharam."));

Tratamento de erros em Promises

// ✅ Um catch no final trata toda a cadeia
buscarUsuario(1)
  .then(usuario => buscarPedidos(usuario.id))
  .then(pedidos => processarPedidos(pedidos))
  .catch(erro => console.error(erro.message));

// ✅ Catch intermediário — recupera e continua
buscarConfiguracoes()
  .catch(erro => {
    console.warn("Configurações não encontradas. Usando padrões.");
    return { tema: "claro", idioma: "pt-BR" }; // valor de fallback
  })
  .then(config => {
    // config é o valor real OU o fallback
    aplicarConfiguracoes(config);
  });

// ⚠️ Promessa rejeitada sem catch = UnhandledPromiseRejection
// Sempre adicione .catch() nas suas Promises
buscarUsuario(-1); // ⚠️ sem catch — aviso no console
buscarUsuario(-1).catch(e => console.error(e)); // ✅

Exemplo completo — Dashboard de dados

// Funções que retornam Promises (simulando APIs)
function buscarEstatisticas() {
  return new Promise(resolve =>
    setTimeout(() => resolve({
      usuarios: 1284,
      pedidos: 342,
      receita: 48750.90,
      crescimento: 12.4,
    }), 800)
  );
}

function buscarUltimosPedidos() {
  return new Promise(resolve =>
    setTimeout(() => resolve([
      { id: 1001, cliente: "Ana", total: 250, status: "entregue" },
      { id: 1002, cliente: "Bruno", total: 180, status: "enviado" },
      { id: 1003, cliente: "Clara", total: 95, status: "pendente" },
    ]), 600)
  );
}

function buscarAlertas() {
  return new Promise(resolve =>
    setTimeout(() => resolve([
      { tipo: "aviso", mensagem: "Estoque baixo: Produto #42" },
      { tipo: "info", mensagem: "3 novos usuários hoje" },
    ]), 400)
  );
}

function buscarMetasMes() {
  return new Promise(resolve =>
    setTimeout(() => resolve({
      meta: 50000,
      atual: 48750.90,
      porcentagem: 97.5,
    }), 700)
  );
}

// Carrega tudo em paralelo — eficiente!
function carregarDashboard() {
  console.log("⏳ Carregando dashboard...");
  const inicio = Date.now();

  Promise.all([
    buscarEstatisticas(),
    buscarUltimosPedidos(),
    buscarAlertas(),
    buscarMetasMes(),
  ])
    .then(([stats, pedidos, alertas, metas]) => {
      const tempo = Date.now() - inicio;
      console.log(`✅ Dashboard carregado em ${tempo}ms\n`);

      // Estatísticas
      console.log("📊 ESTATÍSTICAS");
      console.log(`  Usuários: ${stats.usuarios.toLocaleString("pt-BR")}`);
      console.log(`  Pedidos: ${stats.pedidos}`);
      console.log(`  Receita: R$ ${stats.receita.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`);
      console.log(`  Crescimento: +${stats.crescimento}%\n`);

      // Meta do mês
      console.log("🎯 META DO MÊS");
      const barraSize = 20;
      const preenchido = Math.round((metas.porcentagem / 100) * barraSize);
      const barra = "█".repeat(preenchido) + "░".repeat(barraSize - preenchido);
      console.log(`  [${barra}] ${metas.porcentagem}%`);
      console.log(`  R$ ${metas.atual.toLocaleString("pt-BR")} de R$ ${metas.meta.toLocaleString("pt-BR")}\n`);

      // Últimos pedidos
      console.log("🛍️ ÚLTIMOS PEDIDOS");
      pedidos.forEach(p => {
        const icone = { entregue: "✅", enviado: "📦", pendente: "⏳" }[p.status];
        console.log(`  ${icone} #${p.id} — ${p.cliente}: R$ ${p.total}`);
      });

      // Alertas
      if (alertas.length > 0) {
        console.log("\n🔔 ALERTAS");
        alertas.forEach(a => {
          const icone = a.tipo === "aviso" ? "⚠️" : "ℹ️";
          console.log(`  ${icone} ${a.mensagem}`);
        });
      }
    })
    .catch(erro => {
      console.error(`❌ Erro ao carregar dashboard: ${erro.message}`);
    })
    .finally(() => {
      console.log("\n── Fim do carregamento ──");
    });
}

carregarDashboard();

Comparativo: Callbacks vs Promises

// ── Callbacks ──────────────────────────────
buscarUsuario(1, function(err, usuario) {
  if (err) return tratarErro(err);
  buscarPedidos(usuario.id, function(err, pedidos) {
    if (err) return tratarErro(err);
    processarPedidos(pedidos, function(err, resultado) {
      if (err) return tratarErro(err);
      console.log(resultado);
    });
  });
});

// ── Promises ───────────────────────────────
buscarUsuario(1)
  .then(usuario => buscarPedidos(usuario.id))
  .then(pedidos => processarPedidos(pedidos))
  .then(resultado => console.log(resultado))
  .catch(erro => tratarErro(erro));

A diferença é clara. E ainda vai melhorar no próximo artigo, quando chegarmos ao async/await.

Tarefa para você

Implemente um sistema de checkout de e-commerce com Promises:

// Implemente as funções abaixo retornando Promises com setTimeout:

// validarCarrinho(itens)
//   → resolve com itens se válido
//   → rejeita se carrinho estiver vazio

// verificarEstoque(itens)
//   → resolve com itens se todos têm estoque
//   → rejeita com qual produto está sem estoque

// calcularFrete(cep)
//   → resolve com { valor, prazo }
//   → rejeita se CEP inválido (menos de 8 dígitos)

// processarPagamento(total, cartao)
//   → resolve com { aprovado: true, codigo }
//   → rejeita se cartao.limite < total

// confirmarPedido(dados)
//   → resolve com { numeroPedido, previsaoEntrega }

// Monte a cadeia com Promise.all onde possível
// (verificarEstoque e calcularFrete podem ser paralelos)
// Trate cada tipo de erro com mensagem específica
Ver solução — checkout completo com Promises
// ---------------------------------------------------------------
// Catálogo e helpers
// ---------------------------------------------------------------
const ESTOQUE = { caneca: 12, camiseta: 0, adesivo: 300 };
const PRECOS  = { caneca: 39.9, camiseta: 79.9, adesivo: 5.0 };

// Todo passo simula latência de rede. O atraso vem antes da decisão,
// então o erro chega assíncrono como chegaria numa API de verdade.
const daquiA = (ms) => (fn) =>
  new Promise((resolve, reject) => setTimeout(() => fn(resolve, reject), ms));

const subtotal = (itens) =>
  itens.reduce((soma, i) => soma + PRECOS[i.sku] * i.quantidade, 0);

// ---------------------------------------------------------------
// Os cinco passos
// ---------------------------------------------------------------
function validarCarrinho(itens) {
  return daquiA(200)((resolve, reject) => {
    if (!Array.isArray(itens) || itens.length === 0) {
      reject(new Error('CARRINHO_VAZIO: adicione ao menos um item.'));
      return;
    }
    const invalido = itens.find((i) => !PRECOS[i.sku] || i.quantidade < 1);
    if (invalido) {
      reject(new Error(`ITEM_INVALIDO: ${invalido.sku ?? 'sem sku'}`));
      return;
    }
    resolve(itens);
  });
}

function verificarEstoque(itens) {
  return daquiA(400)((resolve, reject) => {
    const faltando = itens.find((i) => (ESTOQUE[i.sku] ?? 0) < i.quantidade);
    if (faltando) {
      reject(new Error(`SEM_ESTOQUE: ${faltando.sku}`));
      return;
    }
    resolve(itens);
  });
}

function calcularFrete(cep) {
  return daquiA(300)((resolve, reject) => {
    const limpo = String(cep).replace(/\D/g, '');
    if (limpo.length < 8) {
      reject(new Error(`CEP_INVALIDO: ${cep}`));
      return;
    }
    // Região pelo primeiro dígito, só para variar o resultado.
    const regiao = Number(limpo[0]);
    resolve({ valor: 12.9 + regiao * 2.1, prazo: 3 + regiao });
  });
}

function processarPagamento(total, cartao) {
  return daquiA(700)((resolve, reject) => {
    if (cartao.limite < total) {
      reject(new Error(
        `LIMITE_INSUFICIENTE: precisa de ${total.toFixed(2)}, ` +
        `disponível ${cartao.limite.toFixed(2)}`
      ));
      return;
    }
    resolve({
      aprovado: true,
      codigo: `AUT-${Math.random().toString(36).slice(2, 10).toUpperCase()}`,
    });
  });
}

function confirmarPedido(dados) {
  return daquiA(250)((resolve) => {
    const previsao = new Date();
    previsao.setDate(previsao.getDate() + dados.frete.prazo);

    resolve({
      numeroPedido: `PED-${Date.now().toString().slice(-8)}`,
      previsaoEntrega: previsao.toLocaleDateString('pt-BR'),
      total: dados.total,
      autorizacao: dados.pagamento.codigo,
    });
  });
}

// ---------------------------------------------------------------
// A cadeia
// ---------------------------------------------------------------
function checkout({ itens, cep, cartao }) {
  return validarCarrinho(itens)
    .then((itensValidos) =>
      // Estoque e frete não dependem um do outro: rodam juntos.
      // Em série seriam 400 + 300 ms; assim, os mesmos 400 ms do mais lento.
      Promise.all([verificarEstoque(itensValidos), calcularFrete(cep)])
    )
    .then(([itensOk, frete]) => {
      const total = subtotal(itensOk) + frete.valor;

      // Devolver o objeto inteiro mantém itens e frete vivos no próximo elo —
      // sem isso, confirmarPedido não teria o prazo para calcular a entrega.
      return processarPagamento(total, cartao).then((pagamento) => ({
        itens: itensOk,
        frete,
        total,
        pagamento,
      }));
    })
    .then(confirmarPedido)
    .then((pedido) => {
      console.log(`✅ Pedido ${pedido.numeroPedido} confirmado`);
      console.log(`   Total: R$ ${pedido.total.toFixed(2)}`);
      console.log(`   Entrega até ${pedido.previsaoEntrega}`);
      return pedido;
    })
    .catch((erro) => {
      // Um .catch() no fim pega a rejeição de qualquer elo anterior.
      // O prefixo do código dá a mensagem específica de cada falha.
      const [codigo] = erro.message.split(':');

      const mensagens = {
        CARRINHO_VAZIO:      'Seu carrinho está vazio.',
        ITEM_INVALIDO:       'Há um item inválido no carrinho.',
        SEM_ESTOQUE:         'Um produto ficou sem estoque durante a compra.',
        CEP_INVALIDO:        'CEP inválido — confira o endereço de entrega.',
        LIMITE_INSUFICIENTE: 'Pagamento recusado por limite insuficiente.',
      };

      console.error(`❌ ${mensagens[codigo] ?? 'Falha inesperada no checkout.'}`);
      console.error(`   Detalhe técnico: ${erro.message}`);

      // Repropaga: quem chamou decide o que fazer (retentar, avisar a UI…).
      throw erro;
    });
}

// ---------------------------------------------------------------
// Testando
// ---------------------------------------------------------------
checkout({
  itens:  [{ sku: 'caneca', quantidade: 2 }, { sku: 'adesivo', quantidade: 5 }],
  cep:    '01310-100',
  cartao: { limite: 500 },
});

// Caminhos de erro:
// checkout({ itens: [], cep: '01310100', cartao: { limite: 500 } });
//   → CARRINHO_VAZIO
// checkout({ itens: [{ sku: 'camiseta', quantidade: 1 }], cep: '01310100', cartao: { limite: 500 } });
//   → SEM_ESTOQUE
// checkout({ itens: [{ sku: 'caneca', quantidade: 1 }], cep: '123', cartao: { limite: 500 } });
//   → CEP_INVALIDO
// checkout({ itens: [{ sku: 'caneca', quantidade: 2 }], cep: '01310100', cartao: { limite: 10 } });
//   → LIMITE_INSUFICIENTE

Dois pontos que o exercício cobra: Promise.all só vale onde os passos são independentes — pagamento depende do frete, então fica em série; e o return dentro do .then() é o que encadeia. Sem ele a Promise seguinte não é aguardada e o .catch() final nunca vê o erro.

Uma Promise tem três estados e uma regra que resolve metade das dúvidas: a transição acontece uma vez só e é definitiva — de pendente para resolvida ou rejeitada, nunca de volta. O encadeamento com .then nasce daí, e os quatro combinadores também, cada um respondendo a uma pergunta diferente: all exige que todas deem certo, allSettled quer o destino de cada uma, race aceita a primeira que se assentar e any espera o primeiro sucesso.

Fontes e Referências

 

Exercícios

Exercício 1

No encadeamento do artigo, a terceira etapa chama buscarPedidos(1) com o número cravado. Por que não dá para escrever buscarPedidos(usuario.id) ali?

buscarUsuario(1)
  .then(usuario => {
    return buscarEndereco(usuario.enderecoId);
  })
  .then(endereco => {
    return buscarPedidos(1); // ← por que não usuario.id?
  })
  .then(pedidos => console.log(pedidos.length));
Ver resposta

✓ Resposta: Porque usuario não existe nesse escopo. Cada .then() recebe uma função nova, e o único dado que chega até ela é o valor devolvido pelo elo anterior — que aqui é o endereço. O usuário ficou para trás, no escopo do primeiro .then. Essa é a costura que a cadeia de promises não consegue esconder, e ela tem três saídas conhecidas. A primeira é aninhar um nível, colocando a segunda chamada dentro do primeiro .then — funciona, mas reintroduz um pedaço da pirâmide que se queria eliminar. A segunda é carregar tudo adiante, devolvendo um objeto em vez de um valor solto: return buscarEndereco(...).then(endereco => ({ usuario, endereco })). A terceira é Promise.all([usuario, buscarEndereco(...)]), aproveitando que o combinador aceita valores comuns misturados a promises. Nenhuma das três é bonita, e é exatamente por isso que o próximo artigo importa: com async/await todos os valores ficam no mesmo escopo, como em qualquer função síncrona, e o problema simplesmente deixa de existir.

Exercício 2

Três promises: A resolve em 100 ms, B rejeita em 50 ms, C resolve em 300 ms. O que cada combinador devolve, e em que instante?

Promise.all([A, B, C]);
Promise.allSettled([A, B, C]);
Promise.race([A, B, C]);
Promise.any([A, B, C]);
Ver resposta

✓ Resposta: Promise.all rejeita aos 50 ms, com o erro de B — basta uma falhar. allSettled resolve aos 300 ms, esperando todas, com três objetos {status, value} ou {status, reason}. race rejeita aos 50 ms: ela corre para a primeira que se assenta, e assentar inclui falhar. any resolve aos 100 ms com o valor de A: ela ignora rejeições e espera o primeiro sucesso, só rejeitando se todas falharem — e nesse caso com um AggregateError, cujo array .errors traz todas as causas. Duas observações que mudam decisões reais de código: o all rejeitar cedo não cancela A nem C, que seguem rodando até o fim com os resultados descartados; e a diferença entre race e any é justamente o que faz do race a ferramenta de timeout — você quer que a falha vença — e do any a de redundância entre servidores, onde a falha de um não pode derrubar a tentativa.

Exercício 3

Falta uma palavra nesta cadeia. O que ela imprime, e o que acontece se buscarPedidos rejeitar?

buscarUsuario(1)
  .then(usuario => { buscarPedidos(usuario.id); })
  .then(pedidos => console.log(pedidos.length))
  .catch(erro => console.error("falhou:", erro.message));
Ver resposta

✓ Resposta: Falta o return, e isso produz dois defeitos de uma vez. O primeiro: a arrow com chaves e sem return devolve undefined, então a cadeia não espera buscarPedidos — o .then seguinte roda imediatamente com pedidos valendo undefined, e undefined.length lança TypeError. Esse erro é capturado pelo .catch, que imprime falhou: Cannot read properties of undefined (reading 'length') — uma mensagem que não aponta em nada para a causa real. O segundo defeito é mais silencioso: a promise de buscarPedidos foi criada e ficou solta, fora da cadeia. Se ela rejeitar, ninguém a observa, e o resultado é um UnhandledPromiseRejection — no navegador, um aviso no console; no Node, um processo encerrado. O .catch do fim jamais a alcança, porque só trata a cadeia à qual está ligado. É por isso que o estilo idiomático é a arrow sem chaves, .then(usuario => buscarPedidos(usuario.id)): o retorno é implícito e o esquecimento fica impossível.

Exercício 4

A requisição termina em 500 ms e ganha a corrida. O que acontece com o temporizador de 3 segundos? E se fosse o contrário — o timeout vencendo — a requisição para?

const timeout = new Promise((_, reject) =>
  setTimeout(() => reject(new Error("Timeout")), 3000)
);

Promise.race([buscarUsuario(1), timeout])
  .then(usuario => console.log(usuario.nome))
  .catch(erro => console.error(erro.message));
Ver resposta

✓ Resposta: O temporizador continua vivo e dispara aos 3 segundos, mesmo com a corrida já decidida. Rejeitar uma promise que ninguém mais observa não causa erro visível — a race já se assentou e ignora o segundo resultado —, mas o setTimeout segue agendado, segurando na memória o callback e tudo o que ele fecha por closure; no Node, ele ainda impede o processo de terminar até disparar. Em código que faz isso a cada requisição, são milhares de temporizadores pendurados. A correção é guardar o id e limpá-lo num .finally() da corrida. Quanto à segunda pergunta: não, a requisição não para. A race escolhe de quem você vai ouvir a resposta, não quem continua trabalhando — a chamada segue até o fim, ocupando conexão, e um efeito colateral no servidor acontece de qualquer jeito. Cancelar de verdade exige que a própria operação saiba ser cancelada, que é o papel do AbortController passado ao fetch pela opção signal.

Exercício 5

buscarUsuario(-1) sempre rejeita. O que cada uma das três cadeias imprime?

// A
buscarUsuario(-1)
  .catch(e => { console.warn("usando padrão"); return { nome: "convidado" }; })
  .then(u => console.log(u.nome));

// B
buscarUsuario(-1)
  .then(u => console.log(u.nome))
  .catch(e => console.warn("erro"));

// C
buscarUsuario(-1)
  .catch(e => console.warn("erro"))
  .then(u => console.log(u.nome));
Ver resposta

✓ Resposta: A imprime usando padrão e depois convidado. B imprime só erro. C imprime erro e em seguida lança TypeError: Cannot read properties of undefined. A chave para as três é que o .catch não encerra a cadeia: ele a recoloca no trilho do sucesso, e o que ele devolve vira o valor do próximo .then. Em A isso é deliberado e é o padrão de fallback — captura a falha, devolve um valor razoável, e a cadeia segue como se nada tivesse acontecido. Em B o .catch está no fim, então o .then anterior é simplesmente pulado quando há rejeição. Em C está o acidente: o console.warn não devolve nada, o .catch resolve com undefined, e o .then seguinte roda assim mesmo. A regra prática que resolve os três casos é decidir a intenção antes de escolher a posição — .catch no fim para relatar, .catch no meio só quando ele devolve um valor de substituição e a cadeia realmente pode continuar.

Comentários

Mais em Javascript

Async/Await: escrevendo código assíncrono de forma limpa
Async/Await: escrevendo código assíncrono de forma limpa

A cadeia de .then resolveu a pirâmide mas espalhou os valores por escopos…

Tratamento de erros em requisições HTTP
Tratamento de erros em requisições HTTP

Código que só funciona quando tudo dá certo não está pronto para produção…

O que é JavaScript e por que aprender?
O que é JavaScript e por que aprender?

Criada em dez dias, em 1995, e ainda assim a única linguagem que todo…