Guia Completo de Promises Internamente: Implementando uma Promise do Zero Já leu

O Que é uma Promise e Por Que Implementar do Zero Uma Promise é um objeto JavaScript que representa a eventual conclusão (ou falha) de uma operação assíncrona e seu valor resultante. Aprender a implementar uma Promise internamente é fundamental para entender como JavaScript gerencia operações assíncronas. Quando você implementa do zero, compreende os estados (pending, fulfilled, rejected), o fluxo de callbacks e por que o padrão é tão poderoso para evitar callback hell. Nesta aula, construiremos uma Promise funcional, respeitando a especificação Promises/A+. Você verá que uma Promise não é "mágica" — é um padrão bem estruturado que você pode reproduzir. Após esta jornada, entenderá por que , e funcionam como funcionam, e poderá até debugar Promises com confiança. Arquitetura Interna: Estados e Transições Os Três Estados de uma Promise Uma Promise começa em estado pending (pendente). Durante a execução do executor (a função passada ao construtor), ela pode fazer uma transição irreversível para fulfilled (cumprida, com um valor)

O Que é uma Promise e Por Que Implementar do Zero

Uma Promise é um objeto JavaScript que representa a eventual conclusão (ou falha) de uma operação assíncrona e seu valor resultante. Aprender a implementar uma Promise internamente é fundamental para entender como JavaScript gerencia operações assíncronas. Quando você implementa do zero, compreende os estados (pending, fulfilled, rejected), o fluxo de callbacks e por que o padrão é tão poderoso para evitar callback hell.

Nesta aula, construiremos uma Promise funcional, respeitando a especificação Promises/A+. Você verá que uma Promise não é "mágica" — é um padrão bem estruturado que você pode reproduzir. Após esta jornada, entenderá por que .then(), .catch() e .finally() funcionam como funcionam, e poderá até debugar Promises com confiança.

Arquitetura Interna: Estados e Transições

Os Três Estados de uma Promise

Uma Promise começa em estado pending (pendente). Durante a execução do executor (a função passada ao construtor), ela pode fazer uma transição irreversível para fulfilled (cumprida, com um valor) ou rejected (rejeitada, com uma razão/erro).

class MinhaPromise {
  constructor(executor) {
    this.state = 'pending'; // pending | fulfilled | rejected
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(cb => cb(value));
      }
    };

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(cb => cb(reason));
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
}

// Uso
new MinhaPromise((resolve, reject) => {
  setTimeout(() => resolve('Sucesso!'), 1000);
});

O executor é chamado imediatamente com as funções resolve e reject. A transição de estado só ocorre uma vez — chamadas subsequentes são ignoradas. Os callbacks são armazenados para execução quando a Promise se resolver.

Implementando .then(), .catch() e Encadeamento

O Método then() e Chain-ability

O .then() retorna uma nova Promise, não a original. Isso permite encadeamento. A novidade aqui é a resolução de Promises intermediárias — se um callback retorna outra Promise, a Promise filha aguarda sua conclusão.

class MinhaPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.state !== 'pending') return;
      this.state = 'fulfilled';
      this.value = value;
      this.onFulfilledCallbacks.forEach(cb => cb());
    };

    const reject = (reason) => {
      if (this.state !== 'pending') return;
      this.state = 'rejected';
      this.reason = reason;
      this.onRejectedCallbacks.forEach(cb => cb());
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    // Garantir que são funções
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : x => x;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };

    return new MinhaPromise((resolve, reject) => {
      const handleFulfilled = () => {
        try {
          const result = onFulfilled(this.value);
          resolvePromise(this, result, resolve, reject);
        } catch (error) {
          reject(error);
        }
      };

      const handleRejected = () => {
        try {
          const result = onRejected(this.reason);
          resolvePromise(this, result, resolve, reject);
        } catch (error) {
          reject(error);
        }
      };

      if (this.state === 'fulfilled') {
        setTimeout(handleFulfilled, 0);
      } else if (this.state === 'rejected') {
        setTimeout(handleRejected, 0);
      } else {
        this.onFulfilledCallbacks.push(handleFulfilled);
        this.onRejectedCallbacks.push(handleRejected);
      }
    });
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  finally(onFinally) {
    return this.then(
      value => MinhaPromise.resolve(onFinally()).then(() => value),
      reason => MinhaPromise.resolve(onFinally()).then(() => { throw reason; })
    );
  }

  static resolve(value) {
    if (value instanceof MinhaPromise) return value;
    return new MinhaPromise(resolve => resolve(value));
  }

  static reject(reason) {
    return new MinhaPromise((_, reject) => reject(reason));
  }
}

function resolvePromise(promise, x, resolve, reject) {
  if (x === promise) {
    reject(new TypeError('Circular reference'));
    return;
  }

  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      const then = x.then;
      if (typeof then === 'function') {
        then.call(
          x,
          y => resolvePromise(promise, y, resolve, reject),
          r => reject(r)
        );
        return;
      }
    } catch (error) {
      reject(error);
      return;
    }
  }

  resolve(x);
}

// Teste
new MinhaPromise((resolve) => {
  resolve(5);
})
.then(value => {
  console.log(value); // 5
  return value * 2;
})
.then(value => {
  console.log(value); // 10
  return new MinhaPromise(res => res(value + 10));
})
.then(value => console.log(value)) // 20
.catch(err => console.error(err));

Mecanismo de Resolução (Thenable)

A função resolvePromise é crítica: ela detecta quando um callback retorna um objeto "thenable" (que possui .then()) e aguarda sua resolução. Isso permite composição de Promises e é o coração do encadeamento. A função usa setTimeout para garantir execução assíncrona, evitando bloqueios.

Tratamento de Erros e Casos Especiais

Captura de Exceções e Propagação

Erros lançados em executores são automaticamente convertidos em rejeições. Quando um callback lança erro, a Promise resultante é rejeitada. O .catch() é apenas sintaxe para .then(null, handler).

// Erro no executor
new MinhaPromise((resolve, reject) => {
  throw new Error('Boom!');
})
.catch(err => console.error('Capturado:', err.message));

// Erro em callback
MinhaPromise.resolve(10)
.then(value => {
  throw new Error('Erro no then');
})
.catch(err => console.log('Tratado:', err.message));

// Propagação de erro
MinhaPromise.reject('Motivo')
.then(x => x * 2) // Pulado
.then(x => x + 5) // Pulado
.catch(reason => console.log('Final:', reason)); // 'Motivo'

A beleza desta arquitetura é que erros propagam automaticamente pela chain até encontrar um .catch(). Sem tratamento, o erro é "silenciosamente perdido" em Promises nativas — implementações reais têm mecanismos para avisar sobre rejeições não tratadas.

Conclusão

Três aprendizados principais: (1) Uma Promise é um gerenciador de estado que transiciona entre pendente, cumprido e rejeitado, executando callbacks registrados no tempo certo; (2) O .then() retorna sempre uma nova Promise, permitindo encadeamento transparente através do mecanismo de resolução de Promises intermediárias; (3) Erros são parte integral do fluxo — tratados como rejeições que propagam pela corrente até serem capturados ou descartados.

Implementar do zero revela que Promises não contêm "magia assíncrona" — elas organizam callbacks de forma previsível. Agora você pode ler código assíncrono com confiança, entender freezes e debugar problemas de timing. Na próxima vez que usar async/await (que é açúcar sintático sobre Promises), saberá exatamente o que está acontecendo internamente.

Referências


Artigos relacionados