Artigo original: How to Write a JavaScript Promise

O que é uma promise ?

Uma promise em Javascript é um objeto que representa a conclusão ou o fracasso de uma tarefa assíncrona e seu valor resultante.¹

Fim.

Estou brincando, é claro. Então, o que essa definição significa?

Primeiramente, muitas coisas em Javascript são objetos. É possível criar um objeto de maneiras diferentes. A maneira mais comum é com a sintaxe literal de objeto:

const myCar = {
   color: 'blue',
   type: 'sedan',
   doors: '4',
};

Você pode também criar uma class (classe) e instanciá-la com a palavra chave new (nova).

class Car {
   constructor(color, type, doors) {
      this.color = color;
      this.type = type;
      this.doors = doors
   }
}

const myCar = new Car('blue', 'sedan', '4');

console.log(myCar);

1_QUB10cb7QMBVBEGM2JRo1g

Uma promise é, simplesmente, um objeto que criamos, como no exemplo posterior. Nós a instanciamos com a palavra chave new. Em vez dos três parâmetros que passamos para fazer nosso carro (color - cor, type - tipo, doors - portas), nós passamos uma função que recebe dois argumentos: resolve (resolver) e reject (rejeitar).

No fim, promises nos dizem algo sobre a conclusão da função assíncrona da qual a retornamos – se deu certo ou não. Dizemos que a função foi bem-sucedida se a promise resolved (foi resolvida) e que não foi bem-sucedida se a promise rejected (foi rejeitada).

const myPromise = new Promise(function(resolve, reject) {});

console.log(myPromise);

1_z8UFY0q1iVmr4xzOqOvFlA
Observe que o status da promise é “pending” (pendente).
const myPromise = new Promise(function(resolve, reject) {
   resolve(10);
});
1_voamRd9sJg_NZ0vOdbYJgg
Observe que resolvemos a promise com o valor 10.

Veja, nada muito assustador – apenas um objeto que criamos. Se nós o expandirmos um pouco:

1_szpVAwKfKzMasjP9Wlpigg
Observe que temos alguns métodos que temos acesso “then” e “catch”

Além disso, podemos passar qualquer coisa que queiramos para resolver e rejeitar. Por exemplo, poderíamos passar um objeto em vez de uma string:

return new Promise((resolve, reject) => {
   if(somethingSuccesfulHappened) {
      const successObject = {
         msg: 'Success',
         data,//...alguns dados que recebemos
      }
      resolve(successObject); 
   } else {
      const errorObject = {
         msg: 'An error occured',
         error, //...algum erro recebido
      }
      reject(errorObject);
   }
});

Ou, como vimos anteriormente, não precisamos passar nada:

return new Promise((resolve, reject) => {
   if(somethingSuccesfulHappend) {
      resolve()
   } else {
      reject();
   }
});

E quanto à parte “assíncrona” da definição?

JavaScript opera a partir de uma única thread. Isso significa que consegue executar apenas uma coisa de cada vez. Imagine uma estrada. Você pode pensar em JavaScript como uma rodovia de pista única. Certos códigos (código assíncrono) podem ceder lugar permitindo que outra parte do código o passe (seja executado primeiro). Quando esse código assíncrono termina sua tarefa, ele retorna à estrada.

Como observação, podemos retornar uma promise a partir de qualquer função. Não há necessidade de ser assíncrona. Dito isso, promises geralmente são retornadas nos casos em que a função da qual retornam é assíncrona. Por exemplo, uma API que possui métodos para salvar dados em um servidor seria uma ótima candidata para retornar uma promise!

Concluindo:

Promises nos dão uma maneira de aguardar que o nosso código assíncrono seja concluído, capturemos valores a partir disso e passemos esses valores a outras partes do nosso programa.

Eu tenho um artigo aqui que se aprofunda nesses conceitos: Thrown For a Loop: Understanding Loops and Timeouts in JavaScript (texto em inglês).

Como usamos uma promise?

Usar uma promise também é chamado de consumir uma promise. No nosso exemplo acima, nossa função retornou um objeto promise. Isso nos permite usar o encadeamento de métodos com nossa função.

Aqui está um exemplo de encadeamento de métodos que eu aposto que você já viu:

const a = 'Some awesome string';
const b = a.toUpperCase().replace('ST', '').toLowerCase();

console.log(b); // some awesome ring

Agora, chamamos nossa promise (fictícia):

const somethingWasSuccesful = true;

function someAsynFunction() {
   return new Promise((resolve, reject){
      if (somethingWasSuccesful) {
         resolve();     
      } else {
         reject()
      }
   });
}

Consumindo nossa promise usando encadeamento de métodos, temos:

someAsyncFunction
   .then(runAFunctionIfItResolved(withTheResolvedValue))
   .catch(orARunAfunctionIfItRejected(withTheRejectedValue));

Um exemplo (mais) real

Imagine que você tem uma função que obtém usuários de um banco de dados. Escrevi uma função de exemplo no CodePen, que simula uma API que você pode usar. Ela fornece duas opções para acessar os resultados. Primeiro, você pode prover uma função de callback onde você pode acessar o usuário ou qualquer erro. Em segundo, a função retorna uma promise como forma de acessar o usuário ou o erro.

Tradicionalmente, acessamos os resultados do código assíncrono através do uso de callbacks.

rr someDatabaseThing(maybeAnID, function(err, result)) {
   //...Depois que obtemos o que vamos buscar no banco de dados...
   if(err) {
      doSomethingWithTheError(error)
   }   else {
      doSomethingWithResults(results);
   }
}

O uso de callbacks é aceitável até que eles se tornem excessivamente aninhados. Em outras palavras, você precisa executar mais código assíncrono a cada novo resultado. Esse padrão de callbacks dentro de callbacks pode levar a algo conhecido como "callback hell" (inferno das callbacks).

1_DxEgvtymVuqpLOSx8NJ57A
O início do callback hell

Promises nos oferecem uma maneira mais elegante e legível de ver o fluxo do nosso programa.

doSomething()
   .then(doSomethingElse) // e se você não se importar
   .catch(anyErrorsPlease);

Escrevendo nossa promise: Cachinhos Dourados, os três ursos e um supercomputador

Imagine que você encontrou uma tigela de sopa. Você gostaria de saber a temperatura da sopa antes de comê-la. Porém, você está sem termômetros. Felizmente, você possui acesso a um supercomputador capaz de dizer a temperatura da tigela de sopa. Infelizmente, esse supercomputador pode levar até 10 segundos para obter os resultados.

1_XtBW084Eg2feXeR97W2yvw

Algumas coisas para observarmos.

  1. Iniciamos a variável global chamada result.
  2. Simulamos a duração do delay (atraso) da rede com  Math.random() e setTimeout().
  3. Simulamos a temperatura com Math.random().
  4. Mantemos os valores de delay e de temperatura confinados dentro de um range (intervalo) adicionando "matemática" extra. O range para temp é de 1 a 300; o range para o delay é 1000ms a 10000ms (1 a 10 segundos).
  5. Registramos o delay e a temperatura para ter uma ideia de quanto tempo essa função levará e os resultados que esperamos ver quando estiver concluída.

Execute a função e registre os resultados.

getTemperature(); 
console.log(results); // undefined

A temperatura é undefined (indefinida). O que aconteceu?

A função levará um certo tempo para ser executada . A variável não é definida até que o delay acabe. Então, enquanto executamos a função, setTimeout é assíncrona. A parte do código em setTimeout sai do thread principal para uma área de espera.

Tenho um artigo aqui que se aprofunda nesse processo: Thrown For a Loop: Understanding Loops and Timeouts in JavaScript (texto em inglês).

Como a parte da nossa função que define a variável result se move para uma área de espera até que seja concluída, nosso analisador está livre para passar para a próxima linha. No nosso caso, é nosso console.log(). Nele, result ainda é undefined pois, nosso setTimeout não acabou.

Então, o que mais poderíamos tentar ? Poderíamos executar getTemperature() e aguardar 11 segundos (já que o nosso delay máximo e de 10 segundos) e then (então) fazer o console.log dos resultados.

getTemperature();
   setTimeout(() => {
      console.log(result); 
   }, 11000);
   
// Muito quente | Delay: 3323 | Temperatura: 209 graus

Isso funciona, mas o problema com essa técnica é que, embora em nosso exemplo saibamos o delay máximo da rede, em um exemplo da vida real, pode levar ocasionalmente mais de 10 segundos. Mesmo que pudéssemos garantir um delay máximo de 10 segundos, se o resultado ficar pronto antes, haverá um desperdício de tempo.

As promises salvam o dia

Vamos refatorar nossa função getTemperature() para retornar uma promise. Em vez de definir o resultado, rejeitaremos a promise a menos que o resultado seja "na medida", caso em que resolveremos a promise. Em ambos os casos, passaremos alguns valores para resolver e rejeitar.

1_4RJERRgVUtHlIYRFm2piVQ

Agora, podemos usar os resultados da nossa promise que estamos retornando (também chamado de consumir a promise).

getTemperature()
   .then(result => console.log(result))
   .catch(error => console.log(error));
   
// Rejeitar: muito fria | Delay: 7880 | Temperatura: 43 graus

.then será chamado quando nossa promise for resolvida e retornará qualquer informação que passamos para o resolve.

.catch será chamado quando nossa promise for rejeitada e retornará qualquer informação que passamos para o reject.

Muito provavelmente, você consumirá mais promises do que as criará. Em ambos os casos, elas ajudam a tornar o código mais elegante, legível e eficiente.

Resumo

  1. Promises são objetos que contém informações sobre a conclusão de algum código assíncrono e quaisquer valores resultantes que queremos passar.
  2. Para retornar uma promise usamos return new Promise((resolve, reject)=> {})
  3. Para consumir uma promise usamos .then, para obter as informações de uma promise que foi resolvida, e .catch, para obter as informações de uma promise que foi rejeitada.
  4. Você provavelmente usará (consumirá) promises mais do que as escreverá.

Referências

  1. https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Promise