Cómo escribir una promesa de JavaScript

¿Qué es una promesa?

Una promesa de JavaScript es un objeto que representa la finalización o el fracaso de una tarea asincrónica 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. Puede crear un objeto de diferentes formas. La forma más común es con la sintaxis literal de objeto:

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

También puede crear un classe instanciarlo con la newpalabra clave.

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);

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

En última instancia, las promesas nos dicen algo sobre la finalización de la función asincrónica de la que la devolvimos, si funcionó o no. Decimos que la función fue exitosa al decir que la promesa se resolvió y que no tuvo éxito al decir que la promesa fue rechazada.

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

console.log(myPromise);

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

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

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

return new Promise((resolve, reject) => { if(somethingSuccesfulHappened) { const successObject = { msg: 'Success', data,//...some data we got back } resolve(successObject); } else { const errorObject = { msg: 'An error occured', error, //...some error we got back } reject(errorObject); } });

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

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

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

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

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

La comida para llevar:

Las promesas nos dan una manera de esperar a que se complete nuestro código asincrónico, capturar algunos valores de él y pasar esos valores a otras partes de nuestro programa.

Tengo un artículo aquí que profundiza en estos conceptos: Thrown For a Loop: Understanding Loops and Timeouts in JavaScript.

¿Cómo usamos una promesa?

Usar 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 método de encadenamiento con nuestra función.

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

const a = 'Some awesome string'; const b = a.toUpperCase().replace('ST', '').toLowerCase(); console.log(b); // some awesome ring

Ahora, recuerde nuestra (fingida) promesa:

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

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

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

Un (más) ejemplo real.

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

Tradicionalmente, accederíamos a los resultados del código asincrónico mediante el uso de devoluciones de llamada.

rr someDatabaseThing(maybeAnID, function(err, result)) { //...Once we get back the thing from the database... if(err) { doSomethingWithTheError(error) } else { doSomethingWithResults(results); } }

El uso de devoluciones de llamada está bien hasta que se aniden demasiado. En otras palabras, debe ejecutar más código asincrónico con cada nuevo resultado. Este patrón de devoluciones de llamada dentro de las devoluciones de llamada puede llevar a algo conocido como "infierno de devolución de llamada".

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

doSomething() .then(doSomethingElse) // and if you wouldn't mind .catch(anyErrorsPlease);

Writing our own promise: Goldilocks, the Three Bears, and a Supercomputer

Imagine you found a bowl of soup. You’d like to know the temperature of that soup before you eat it. You're out of thermometers, but luckily, you have access to a supercomputer that tells you the temperature of the bowl of soup. Unfortunately, this supercomputer can take up to 10 seconds to get the results.

Here are a couple of things to notice.

  1. We initiate a global variable called result.
  2. We simulate the duration of the network delay with Math.random() and setTimeout().
  3. We simulate a temperature with Math.random().
  4. We keep the delay and temperature values confined within a range by adding some extra “math”. The range for temp is 1 to 300; the range for delay is 1000ms to 10000ms (1s to 10 seconds).
  5. We log the delay and temperature so we have an idea of how long this function will take and the results we expect to see when it’s done.

Run the function and log the results.

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

The temperature is undefined. What happened?

The function will take a certain amount of time to run. The variable is not set until the delay is over. So while we run the function, setTimeout is asynchronous. The part of the code in setTimeout moves out of the main thread into a waiting area.

I have an article here that dives deeper into this process: Thrown For a Loop: Understanding Loops and Timeouts in JavaScript.

Since the part of our function that sets the variable result moves into a holding area until it is done, our parser is free to move onto the next line. In our case, it’s our console.log(). At this point, result is still undefined since our setTimeout is not over.

So what else could we try? We could run getTemperature() and then wait 11 seconds (since our max delay is ten seconds) and then console.log the results.

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

This works, but the problem with this technique is, although in our example we know the maximum network delay, in a real-life example it might occasionally take longer than ten seconds. And, even if we could guarantee a maximum delay of ten seconds, if the result is ready sooner, we are wasting time.

Promises to the Rescue

We are going to refactor our getTemperature() function to return a promise. And instead of setting the result, we will reject the promise unless the result is “Just Right,” in which case we will resolve the promise. In either case, we will pass in some values to both resolve and reject.

We can now use the results of our promise we are returning (also know as consuming the promise).

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

.then will get called when our promise resolves and will return whatever information we pass into resolve.

.catch will get called when our promise rejects and will return whatever information we pass into reject.

Most likely, you’ll consume promises more than you will create them. In either case, they help make our code more elegant, readable, and efficient.

Summary

  1. Promises are objects that contain information about the completion of some asynchronous code and any resulting values we want to pass in.
  2. To return a promise we use return new Promise((resolve, reject)=> {})
  3. To consume a promise we use .then to get the information from a promise that has resolved, and .catch to get the information from a promise that has rejected.
  4. You’ll probably use (consume) promises more than you’ll write.

References

1.) //developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise