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

APIs públicas: exemplos práticos
APIs públicas: exemplos práticos

Seis APIs públicas, nenhuma exigindo cadastro ou chave: ViaCEP para endereço…

Introdução ao Node.js: JavaScript fora do navegador
Introdução ao Node.js: JavaScript fora do navegador

Em 2009 o JavaScript saiu da aba do navegador. O Node roda o mesmo motor V8…

Mini Projeto: Calculadora no Console
Mini Projeto: Calculadora no Console

Chegamos ao fim do primeiro módulo, e o jeito de fechá-lo é construindo: uma…