Original article: How to Write a JavaScript Promise

¿Qué es una promesa?

Una promesa de JavaScript es un objeto que representa la finalización o falla de una tarea asíncrona y su valor resultante.¹

El fin.

Estoy bromeando, por supuesto. Entonces, ¿qué significa esa definición?

En primer lugar, muchas cosas en JavaScript son objetos. Puedes crear un objeto de diferentes maneras. La forma más común es con sintaxis literal de objeto:

const miCarro = {
   color: 'azul',
   tipo: 'sedán',
   puertas: '4',
};

También puedes crear un class e instanciarla con la palabra clave new.

class Carro {
   constructor(color, tipo, puertas) {
      this.color = color;
      this.tipo = tipo;
      this.puertas = puertas
   }
}

const miCarro = new Carro('azul', 'sedán', '4');

console.log(miCarro);

1*QUB10cb7QMBVBEGM2JRo1g

Una promesa es simplemente un objeto como el que creamos con el último ejemplo. Lo instanciamos con la palabra clave new. En lugar de los tres parámetros que pasamos para hacer nuestro auto (color, tipo y puertas), pasamos una función que toma dos argumentos: resolve y reject.

En última instancia, las promesas nos dicen algo sobre la finalización de la función asíncrona de la que la devolvimos–si funcionó o no. Decimos que la función tuvo éxito al decir que la promesa se resolvió, y que no tuvo éxito al decir que la promesa se rechazó.

const myPromesa = new Promesa(function(resolve, reject) {});

console.log(myPromesa);

1*z8UFY0q1iVmr4xzOqOvFlA
Observe que la promesa está pendiente ("pending").
const myPromesa = new Promesa(function(resolve, reject) {
   resolve(10);
});
1*voamRd9sJg_NZ0vOdbYJgg
Observe que resolvimos la promesa con el valor 10.

Vez, no es demasiado aterrador, solo un objeto que creamos. Y, si lo ampliamos un poco:

1*szpVAwKfKzMasjP9Wlpigg
Observe que tenemos algunos métodos a los que tenemos acceso, "then" y "catch"

Además, podemos pasar cualquier cosa que nos gustaría a resolve y reject. Por ejemplo, podríamos pasar un objeto en lugar de una cadena:

return new Promesa((resolve, reject) => {
   if(algoExitosoPasó) {
      const objetoDeÉxito = {
         msg: 'Éxito',
         data,//...algunos datos que obtuvimos
      }
      resolve(objetoDeÉxito); 
   } else {
      const objetoDeError = {
         msg: 'Ocurrió un error',
         error, //...algún error que recibimos
      }
      reject(objetoDeError);
   }
});

O, como vimos antes, no tenemos que pasar nada:

return new Promesa((resolve, reject) => {
   if(algoExitosoPasó) {
      resolve()
   } else {
      reject();
   }
});

¿Qué pasa con la parte "asincrónica" de la definición?

JavaScript es de un solo subproceso. Esto significa que solo puede ejecutar una cosa a la vez. Si puedes imaginar una carretera, puedes pensar en JavaScript como una autopista de un solo carril. Cierto código (código asíncrono) puede deslizarse hacia el hombro para permitir que otro código lo pase. Cuando se realiza ese código asíncrono, vuelve a la carretera.

Como nota al margen, podemos devolver una promesa desde cualquier función. No tiene que ser asíncrono. Dicho esto, las promesas normalmente se devuelven en los casos en que la función de la que devuelven es asíncrona. Por ejemplo, un API que tiene métodos para guardar datos en un servidor sería un gran candidato para devolver una promesa.

La conclusión:

Las promesas nos brindan una forma de esperar a que se complete nuestro código asíncrono, capturar algunos valores de él y pasar esos valores a otras partes de nuestro programa.

¿Cómo usamos una promesa?

El uso de una promesa también se llama consumir una promesa. En nuestro ejemplo anterior, nuestra función devuelve un objeto de promesa. Esto nos permite usar el encadenamiento de métodos con nuestra función.

Aquí hay un ejemplo de encadenamiento de métodos que apuesto a que no has visto:

const a = 'Una cadena impresionante';
const b = a.toUpperCase().replace('CA', '').toLowerCase();

console.log(b); // una dena impresionante

Ahora, recuerda nuestra (pretendida) promesa:

const algoFueExitoso = true;

function algunaFunciónAsíncrona() {
   return new Promesa((resolve, reject){
      if (algoFueExitoso) {
         resolve();     
      } else {
         reject()
      }
   });
}

Y, consumiendo nuestra promesa usando el encadenamiento de métodos:

algunaFunciónAsíncrona
   .then(ejecutarUnaFunciónSiSeResuelve(conElValorResuelto))
   .catch(oEjecutarUnaFunciónSiEsRechazada(conElValorRechazado));

Un ejemplo (más) real.

Imagina que tienes una función que obtiene usuarios de una base de datos. He escrito una función de ejemplo en Codepen que simula una API que podrías usar. Proporciona dos opciones para acceder a los resultados. Uno, puede proporcionar una función de retrollamada donde puede acceder al usuario o cualquier error. O dos, la función devuelve una promesa como una forma de acceder al usuario o error.

Tradicionalmente, accederíamos a los resultados del código asíncrono mediante el uso de retrollamadas.

rr algoDeBaseDeDatos(talVezUnID, function(err, result)) {
   //...Una vez que recuperemos la cosa de la base de datos...
   if(err) {
      hacerAlgoConElError(error)
   }   else {
      hacerAlgoConLosResultados(results);
   }
}

El uso de retrollamada está bien hasta que se anidan demasiado. En otras palabras, tiene que ejecutar más código asincrónico con cada nuevo resultado. Este patrón de retrollamada dentro de las retrollamadas puede conducir a algo conocido como "infierno de retrollamada".

1*DxEgvtymVuqpLOSx8NJ57A
Los comienzos del infierno de retrollamada

Las promesas nos ofrecen una forma más elegante y legible de ver el flujo de nuestro programa.

hazAlgo()
   .then(hacerAlgoMás) // y si no te importa
   .catch(cualquierErrorPorFavor);

Escribiendo nuestra propia promesa: Ricitos de Oro, los Tres Osos y una Supercomputadora

Imagina que encuentras un plato de sopa. Te gustaría saber la temperatura de esa sopa antes de comerla. No te quedan termómetros, pero por suerte tienes acceso a una supercomputadora que te dice la temperatura del plato de sopa. Desafortunadamente, esta supercomputadora puede tardar hasta 10 segundos en obtener los resultados.

1*XtBW084Eg2feXeR97W2yvw

Aquí hay un par de cosas para notar.

  1. Iniciamos una variable global llamado result.
  2. Simulamos la duración del retraso de la red con Math.random() y setTimeout().
  3. Simulamos una temperatura con Math.random().
  4. Mantenemos los valores de retardo y temperatura confinados dentro de un rango agregando algunas "matemáticas" adicionales. El rango de temp es de 1 a 300; el rango de delay es de 1000 ms a 10000 ms (1s a 10 segundos).
  5. Registramos el retraso y la temperatura para tener una idea de cuánto tiempo llevará esta función y los resultados que esperamos ver cuando esté lista.

Ejecute la función y registre los resultados.

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

La temperatura no está definida. ¿Qué pasó?

La función tardará un cierto tiempo en ejecutarse. El variable no se establece hasta que finaliza el retraso. Entonces, mientras ejecutamos la función, setTimeout es asíncrono. La parte del código en setTimeout sale del subproceso principal hacia un área de espera.

Dado que la parte de nuestra función que establece el variable de result se mueve a un área de espera hasta que finaliza, nuestro analizador puede pasar a la siguiente línea. En nuestro caso, es nuestro console.log(). En este punto, el result aún no está definido, ya que nuestro setTimeout no ha terminado.

Entonces, ¿qué más podríamos intentar? Podríamos ejecutar getTemperature() y luego esperar 11 segundos (ya que nuestro retraso máximo es de diez segundos) y luego console.log los resultados.

getTemperature();
   setTimeout(() => {
      console.log(result); 
   }, 11000);
   
// Too Hot | Delay: 3323 | Temperature: 209 deg

Esto funciona, pero el problema con esta técnica es que, aunque en nuestro ejemplo conocemos el retraso máximo de la red, en un ejemplo de la vida real, en ocasiones puede tardar más de diez segundos. Y, aunque pudiéramos garantizar un retraso máximo de diez segundos, si el resultado está listo antes, estamos perdiendo el tiempo.

Promesas al Rescate

Vamos a refactorizar nuestra función getTemperature() para devolver una promesa. Y en lugar de establecer el resultado, rechazaremos la promesa a menos que el resultado sea "Exacto", en cuyo caso resolveremos la promesa. En cualquier caso, pasaremos algunos valores para resolver y rechazar.

1*4RJERRgVUtHlIYRFm2piVQ

Ahora podemos usar los resultados de nuestra promesa que estamos devolviendo (también conocido como consumir la promesa).

getTemperature()
   .then(result => console.log(result))
   .catch(error => console.log(error));
   
// Reject: Too Cold | Delay: 7880 | Temperature: 43 deg

.then será llamado cuando nuestra promesa se resuelva y devolverá cualquier información que pasemos a resolve.

Se llamará a .catch cuando nuestra promesa se rechace y devolverá cualquier información que pasemos al reject.

Lo más probable es que consumas más promesas de lo que las creas. En cualquier caso, ayudan a que nuestro código sea más elegante, legible y eficiente.

Resumen

  1. Las promesas son objetos que contienen información sobre la finalización de algún código asíncrono y los valores resultantes que queremos pasar.
  2. Para devolver una promesa usamos return new Promise((resolve, reject)=> {})
  3. Para consumir una promesa usamos .then para obtener la información de una promesa que se ha resuelto y .catch para obtener la información de una promesa que se ha rechazado.
  4. Probablemente, usarás (consumirás) promesas más de lo que escribirás.

Referencias

1.)https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Promise