Promises são objetos que representam a eventual conclusão (ou falha) de uma operação assíncrona e seu valor resultante. Elas são uma forma mais limpa e flexível de lidar com código assíncrono do que callbacks aninhados, evitando o famoso 'callback hell'. Uma Promise pode estar em um de três estados: pendente, resolvida ou rejeitada, e fornece métodos para reagir a essas mudanças de estado.

Nesta aula, vamos explorar como criar Promises, como consumi-las usando then, catch e finally, e como encadear múltiplas operações assíncronas de forma sequencial. Ao final, você será capaz de escrever código assíncrono mais legível e robusto.

Estados

Uma Promise possui três estados possíveis: pending (pendente), fulfilled (resolvida) e rejected (rejeitada). Quando a Promise é criada, ela inicia no estado pendente. Se a operação assíncrona for bem-sucedida, a Promise é resolvida (fulfilled) com um valor. Se ocorrer um erro, ela é rejeitada (rejected) com um motivo (geralmente um erro). Uma vez que a Promise é resolvida ou rejeitada, ela não pode mudar de estado novamente.

O estado de uma Promise é interno e não pode ser acessado diretamente pelo código. Em vez disso, usamos os métodos then, catch e finally para reagir às mudanças de estado. Exemplo conceitual:

const promise = new Promise((resolve, reject) => {
  // Operação assíncrona
  if (sucesso) {
    resolve('Valor de sucesso');
  } else {
    reject(new Error('Algo deu errado'));
  }
});
// Estado inicial: pending
// Se resolve for chamado: fulfilled
// Se reject for chamado: rejected

then/catch/finally

O método then é usado para registrar callbacks que serão executados quando a Promise for resolvida (fulfilled) ou rejeitada (rejected). Ele aceita até dois argumentos: uma função de sucesso e uma função de erro. No entanto, é mais comum usar catch para tratar erros separadamente, melhorando a legibilidade. O método finally é executado independentemente do resultado, sendo útil para limpeza.

Exemplo com then/catch/finally:

const minhaPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const numero = Math.random();
    if (numero > 0.5) {
      resolve('Sucesso! Número: ' + numero);
    } else {
      reject(new Error('Falha! Número: ' + numero));
    }
  }, 1000);
});

minhaPromise
  .then(resultado => {
    console.log(resultado);
  })
  .catch(erro => {
    console.error(erro.message);
  })
  .finally(() => {
    console.log('Operação finalizada');
  });

O finally é útil para esconder indicadores de carregamento ou liberar recursos, independentemente do sucesso ou falha.

Encadeamento

O encadeamento (chaining) de Promises permite executar operações assíncronas em sequência, onde cada then retorna uma nova Promise. Isso evita o aninhamento excessivo e torna o código mais linear. O valor retornado por um then é passado para o próximo then na cadeia. Se uma Promise for rejeitada em qualquer ponto, o fluxo pula para o próximo catch.

Exemplo de encadeamento:

function asyncOperation(valor) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(valor * 2);
    }, 500);
  });
}

asyncOperation(5)
  .then(resultado1 => {
    console.log('Primeiro resultado:', resultado1);
    return asyncOperation(resultado1);
  })
  .then(resultado2 => {
    console.log('Segundo resultado:', resultado2);
    return asyncOperation(resultado2);
  })
  .then(resultado3 => {
    console.log('Terceiro resultado:', resultado3);
  })
  .catch(erro => {
    console.error('Erro:', erro);
  });

// Saída (após 1.5s):
// Primeiro resultado: 10
// Segundo resultado: 20
// Terceiro resultado: 40

Note que se qualquer Promise na cadeia for rejeitada, o catch será chamado imediatamente, ignorando os then subsequentes.

Criando uma Promise

Para criar uma Promise, usamos o construtor new Promise(executor), onde executor é uma função que recebe dois parâmetros: resolve e reject. Dentro do executor, colocamos a operação assíncrona. Quando a operação termina, chamamos resolve com o resultado ou reject com o erro. O executor é executado imediatamente quando a Promise é criada.

Exemplo prático: simular uma requisição HTTP com setTimeout.

function buscarDados(url) {
  return new Promise((resolve, reject) => {
    // Simula requisição assíncrona
    setTimeout(() => {
      if (url === 'https://api.exemplo.com/dados') {
        resolve({ id: 1, nome: 'João' });
      } else {
        reject(new Error('URL inválida'));
      }
    }, 2000);
  });
}

buscarDados('https://api.exemplo.com/dados')
  .then(dados => console.log(dados))
  .catch(erro => console.error(erro));

// Após 2s: { id: 1, nome: 'João' }

É importante lembrar que o executor não deve ser uma função async (a menos que você saiba o que está fazendo), pois o retorno de uma Promise async pode causar comportamentos inesperados. Prefira usar o construtor diretamente.

Boas Práticas

Sempre trate rejeições com catch para evitar erros não capturados. Evite o uso de throw dentro de then sem um catch apropriado. Prefira encadeamento a aninhamento. Use Promise.all para paralelismo quando a ordem não importa. Lembre-se de que Promises são assíncronas, mesmo que resolvidas imediatamente: o callback de then sempre será chamado em um microtask.

Referências

Exercícios

  1. Crie uma Promise que resolve após 2 segundos com a string "Pronto!".
  2. ✓ Resposta:
    const promise = new Promise((resolve) => {
      setTimeout(() => resolve('Pronto!'), 2000);
    });
    promise.then(console.log);
  3. Use .catch() para tratar uma Promise que rejeita com um erro.
  4. ✓ Resposta:
    const promise = new Promise((_, reject) => {
      reject(new Error('Falha'));
    });
    promise.catch(erro => console.error(erro.message));
  5. Encadeie duas Promises: a primeira retorna 10, a segunda multiplica por 2.
  6. ✓ Resposta:
    function asyncDouble(n) {
      return new Promise(resolve => setTimeout(() => resolve(n * 2), 500));
    }
    
    Promise.resolve(10)
      .then(asyncDouble)
      .then(console.log); // 20
  7. Crie uma função que retorna uma Promise que rejeita se o argumento for negativo.
  8. ✓ Resposta:
    function checkPositive(numero) {
      return new Promise((resolve, reject) => {
        if (numero >= 0) {
          resolve('Número positivo');
        } else {
          reject(new Error('Número negativo'));
        }
      });
    }
    
    checkPositive(-1).catch(console.error);
  9. Use finally para logar "Fim" após uma Promise que resolve.
  10. ✓ Resposta:
    Promise.resolve('OK')
      .then(console.log)
      .finally(() => console.log('Fim'));