Tercera (y última) parte de esta serie sobre JavaScript asíncrono ?. Si acabas de aterrizar aquí, te recomiendo que veas antes las partes #1 y #2, ya que esta parte está ligada a las anteriores.
Encadenando promises
Siguiendo con las ventajas de utilizar promises, otra sería que podemos escribir una promise detrás de otra y aprovechar el código anterior, para así recibir los datos de nuestra request en el orden que especifiquemos.
Encadenar promises es la manera de evitar el callback hell del que hablábamos en el capítulo #2. Así, si ahora quisiéramos recibir los datos del archivo mike.json, debemos identificar en qué momento de nuestro código hacerlo. ?️♀️
Efectivamente, el momento para obtener los datos de Mike sería cuando estamos seguros de haber recibido los datos de Billy correctamente. Así que llamamos al método getToDos() ahí y le pasamos el archivo JSON del que queremos obtener la información.
Añadimos al console.log un número para tener claro que ésta es la primera promesa que se va a resolver.
getToDos('toDos/billy.json') .then(data => { console.log('1rst promise resolved!:', data); getToDos('toDos/mike.json'); }) .catch(error => { console.log('promise rejected :(', error); });
La línea
getToDos('toDos/mike.json');
nos devuelve una promesa. Por tanto, podríamos añadirle el método then() a continuación, anidando así un método dentro de otro. Pero esto no acabaría siendo muy diferente al callback hell del que tratamos de huir. ?♀️
En lugar de eso, lo que hacemos es un return del método getToDos(), consiguiendo así que el método then() en el que se encuentra devuelva dicha función (getToDos). Por tanto, ahora sí que le podemos añadir el método then(), pero a todo el bloque.
A ese método then() le pasamos una función (con data como parámetro) que se disparará cuando la request al archivo mike.json se complete sin errores.
Hacemos un console.log de los datos para verlos por consola.
getToDos('toDos/billy.json') .then(data => { console.log('1rst promise resolved!:', data); return getToDos('toDos/mike.json'); }).then(data => { console.log('2nd promise resolved!', data); }) .catch(error => { console.log('promise rejected :(', error); });
¡Y ahí lo tenemos! Los datos de Billy y de Mike, en ese orden. ?♀️
Siguiendo el mismo patrón, vamos a obtener los datos del archivo will.json. Para ello, localizamos el momento en el que ya hemos recibido los datos de Mike y damos los mismos pasos de antes.
getToDos('toDos/billy.json') .then(data => { console.log('1rst promise resolved!:', data); return getToDos('toDos/mike.json'); }).then(data => { console.log('2nd promise resolved!', data); return getToDos('toDos/will.json'); }).then(data => { console.log('3rd promised resolved!', data); }) .catch(error => { console.log('promise rejected :(', error); });
¡Y ahora ya podemos ver toda la información de nuestros archivos JSON en la consola! ?♀️
Otra ventaja de las promises es que el método catch() captura cualquier error que suceda en cualquier http request, por tanto, al contrario que el then(), sólo tenemos que escribirlo una vez. ?
Nos modernizamos: Fetch API
El fetch API es la manera más moderna de hacer http requests. Efectivamente, hasta ahora hemos estado viendo tecnologías que, aunque funcionan perfectamente y son sólidas como rocas, tienen ciertas desventajas de complejidad que la tecnología fetch API viene a solucionar.
Esta nueva tecnología nos permitirá hacer http request de manera más rápida y eficiente.
Está construida directamente en el núcleo de JavaScript, en el window object, vaya. Llamarla es tan fácil como hacer:
fetch();
Otra característica de esta tecnología es que implementa la promise API entre bastidores, es decir, aunque no lo veamos, la fetch API viene con características de las promises. ¡Vamos a verla en la práctica! ??
1. Borra o comenta todo el código anterior de sandbox.js, porque no lo vamos a necesitar.
2. La fetch API no es más que una función a la que podemos pasarle parámetros. Como primer parámetro le pasamos el recurso del que queramos obtener información, ya sea un API endpoint o un archivo JSON nuestro.
En nuestro caso, vamos a apuntar a uno de nuestros archivos JSON, por ejemplo, mike.json.
fetch('toDos/mike.json');
3. Esto nos devuelve una promesa, lo que nos indica que en algún momento esa promesa se resolverá (resolve) si todo ha salido bien, o se rechazará (reject) si ha habido algún error. Sabiendo esto, podemos vincularle los métodos then() y catch().
? Sabemos que el método then() será el que se disparará si todo sale bien, es decir, si la promise ha tomado el camino del resolve().
4. Al método then() le pasamos un parámetro al que llamamos response, y al catch(), un parámetro al que llamamos err. Hacemos un console.log de ambos parámetros para ver lo que sucede.
fetch('toDos/mike.json') .then(response => { console.log('promise resolved: ', response); }) .catch(err => { console.log('promise rejected: ', err); });
Verás que se imprimen una serie de datos, pero en ningún sitio vemos nuestra información del mike.json. Volveremos a eso luego, porque ahora debemos puntualizar algo sobre los errores.
Es importante saber que la tecnología fetch API gestiona los errores de manera un pelín diferente al método más antiguo. Así, lo que sucede es que, aunque hayamos cometido un error por nuestra parte al escribir el API endpoint, la request se seguirá enviando y la promesa, cumpliéndose.
Esto significa que la promesa resultaría en resolve(), pero no nos devolvería ninguna información. ¿Y por qué resulta en resolve()? Porque la fetch API traza la línea entre resolve() y reject() en un punto distinto.
Es decir, para que una promise dentro de una fetch API resulte en reject(), no es suficiente con cometer un error en el API endpoint. Una promesa en el contexto de una fetch API sólo será rechazada cuando haya algún tipo de error en la red, o cuando el API endpoint no esté disponible por algún motivo. ?
Prueba a introducir un error en el recurso pasado al getToDos como parámetro y verás que, aún así, la promise se ejecuta correctamente. Pero si despliegas el Response object devuelto por consola, verás que nos devuelve un status: 404. O sea, un código de error.
5. Visto esto, vamos a corregir nuestro endpoint y ponerlo como estaba antes. Ahora sí, volverás a ver que se imprimen una serie de datos. Todos estos datos están dentro de un objeto (Response) que la fetch API crea cuando hacemos una http request. ?
Y como ya hemos mencionado antes, ese objeto tiene de todo...menos la información en sí del JSON mike.json. Si expandimos el __proto__ , lugar donde se encuentran los métodos de una función, verás que existe uno llamado json().
Lo que podemos hacer es usar ese método json() sobre el Response object, y así obtendremos los datos que necesitamos. ?
El método json() no solamente obtiene los datos de nuestra request, sino que también los traduce al formato correcto (lo que se conoce como parse).
fetch('toDos/mike.json') .then(response => { console.log('promise resolved: ', response); response.json(); }) .catch(err => { console.log('promise rejected: ', err); });
Con la forma tradicional de convertir formatos, antes debíamos crear una variable y almacenar ahí los datos convertidos, tipo:
const data = JSON.parse(request.responseText);
Pero con el método json() eso no funcionaría, porque este método devuelve una promesa. ?
6. Así que lo que hacemos es un return del método json(), y lo vinculamos a un método then() para gestionar lo que sucederá si la promesa se resuelve correctamente (resolve).
Dentro del método then() le pasamos por parámetro los datos que vamos a recibir desde el método json(), y hacemos un console.log de éstos.
fetch('toDos/mike.json') .then(response => { console.log('promise resolved: ', response); return response.json(); }).then(data => { console.log(data); }) .catch(err => { console.log('promise rejected: ', err); });
¡Y ahora sí! En nuestra consola ya podemos ver los datos del archivo mike.json. ? Como ves, hemos logrado esto escribiendo mucho menos código que con el sistema anterior, usando XHR.
Y llegó async y await
Async/await es una adición reciente a JS. Nos permite encadenar promesas de manera incluso más limpia y eficiente que hasta ahora. El sistema anterior usando la fetch API tiene el inconveniente de que, cuando intentemos hacer varias requests y encadenar así varias promesas, nuestro código empezaría a resultar algo confuso. ?
Con asyn/await, lo que conseguimos es reunir todo nuestro código asíncrono en una async function para después usar la palabra await dentro de dicha función para encadenar todas las promesas que necesitemos.
¡Pasemos a la práctica!
1. Comentamos todo nuestro código anterior para que no nos moleste.
2. Creamos la función que va a contener todo nuestro código asíncrono y la llamamos getToDos. Para construir una función asíncrona, lo único que tenemos que hacer es escribir la palabra async delante de los paréntesis.
const getToDos = async () => { };
Ahora, siempre que llamemos a esa función, devolverá una promesa, porque eso es lo que hacen las async functions. Si quieres probarlo, crear una variable con el valor de la función y haz un console.log de la misma. Verás que la consola te devuelve una promesa.
console.log(test); // -> Promise {<resolved>: undefined}
3. Dentro de la función, usamos la fetch API para hacer nuestra request. El lugar desde donde queremos obtener información es toDos/mike.json, por ejemplo.
const getToDos = async () => { fetch('toDos/mike.json') };
Ya que el fetch devuelve una promesa, podríamos vincularle el método then() para que se disparase si la promesa se resolviese correctamente. Pero si usamos await, ya no necesitamos hacer eso. ?
En lugar de eso, lo que hacemos es guardar el fetch en una variable a la que llamamos response. Y lo primero que debemos escribir dentro de esa variable es la keyword "await". Lo que hace JS aquí es no asignar el valor de la variable response a dicha variable hasta que la promesa se haya resuelto.
Esto resulta en un código más sencillo y limpio que usando then().
Recuerda que todo este código que estamos creando es non-blocking, porque es asíncrono, por tanto, JS lo ejecuta en un lugar del navegador que no interrumpe la ejecución del resto de nuestro código.
Si ahora hacemos un console.log de la variable response, en la consola deberíamos ver el mismo Response object que obteníamos con el anterior código.
4. Al igual que antes, tampoco hemos recibido la información del archivo JSON. Para eso tenemos que usar el método json() que ya conocemos. Recordemos que este método es asíncrono por defecto, es decir, devuelve una promesa.
? Por tanto, podemos usar el await sobre él y guardarlo en una variable a la que llamamos data.
5. Comentamos el console.log para que no nos moleste y hacemos otro console.log de la data.
const getToDos = async () => { const response = await fetch('toDos/mike.json'); const data = await response.json(); // console.log(response); console.log(data); };
Y con esto ya deberíamos ver la información del archivo mike.json en nuestra consola. ? ¡Chachi!
6. Ya que lo que queremos es obtener esa información, hacemos un return de la misma (data).
Es crucial entender que toda la función getToDos devuelve una promesa, por tanto, hasta el momento no hemos especificado en ningún sitio qué hacer cuando la promesa sea resuelta o rechazada.
Por eso, si ahora hiciésemos un console.log del getToDos, volveríamos a obtener lo mismo que al principio.
De hecho, en la consola verás que esta JS nos dice que la promesa está pendiente de resolución. Por tanto, tenemos que echar mano del método then() para indicarle a la promesa qué debe hacer si se resuelve correctamente.
7. Así que eliminamos el console.log y la variable test, y vinculamos el método then() a getToDos(). Al then() le pasamos un parámetro por el cual vamos a recibir la información del archivo JSON, así que lo llamamos también data.
Hacemos un console.log diciendo que la promesa se ha resuelto e imprimiendo la data.
getToDos() .then(data => console.log('promise resolved:', data))
Ahora en tu consola deberías ver, por fin, la información del archivo mike.json. ?
Aquí dejo un esquema/línea del tiempo sobre la evolución de JS con respecto a la comunicación con APIs.
Gestionando nuestro errores con throw y catch
En esta sección vamos a aprender a crear nuestros propios "localizadores" de errores dentro de una async function. Dentro de esa función, ocurrirá un error cuando la promesa que alberga dicha función resulte rechazada (reject).
1. El estado actual de nuestro código contempla la posibilidad de que, si existe un error de sintaxis en nuestro archivo JSON, podamos captarlo con el método catch() cuando llamamos a la función getToDos().
getToDos() .then(data => console.log('promise resolved:', data)) .catch(err => console.log('promise rejected:', err))
2. Es decir, si ahora vamos al archivo mike.json y cometemos un error de sintaxis (por ejemplo, quitando unas comillas), nuestro código capturará y nos informará de ese error.
Si vas a tu consola, comprobarás que tenemos un mensaje de error, tipo:
promise rejected: SyntaxError: Unexpected token t in JSON at position 59
3. Podemos utilizar la propiedad message del método catch() para obtener un mensaje más concreto.
.catch(err => console.log('promise rejected:', err.message)); // promise rejected: Unexpected token t in JSON at position 59
Aunque no vamos a conseguir un resultado muy distinto al anterior. ?
4. Reestablecemos el código de nuestro archivo JSON a su estado anterior, sin errores.
5. Si ahora cometemos un error en el nombre del archivo al que apunta la fetch API, lo que sucede es que la promesa que devuelve ese statement no es rechazada, sino que se resuelve correctamente. Esto es algo que que ya hemos comentado en la sección de la fetch API, punto 4. ??
const response = await fetch('toDos/mikes.json');
Por tanto, lo que ocurre es que la promesa que nos devuelve el código de arriba se resuelve correctamente, pero no ocurre lo mismo con la promesa que nos devuelve este statement:
const data = await response.json();
Ya que esta promesa será rechazada, devolviéndonos un error en la consola:
promise rejected: Unexpected token < in JSON at position 0
El problema con este error es que no es del todo preciso, porque nos dice que existe un error en el archivo JSON, lo cual no es cierto. Porque el error se produce en el endpoint, ya que lo hemos escrito mal. ?
5. Para lidiar con este problema, debemos comprobar manualmente si el status de la response es 200 o no. En caso de no tener un status de 200, significa que ha habido un error en el nombre del endpoint, y por tanto podemos crear una manera de gestionarlo para así poder identificarlo. Es lo que se conoce como "throw an error", o lanzar/gestionar tu propio error.
Para ello, creamos un nuevo Error object usando la keyword "new" y lo gestionamos usando la keyword "throw". Dentro de este objeto podemos pasarle un mensaje de error, que pasará a ser la propiedad message del err en el método catch().
const getToDos = async () => { const response = await fetch('toDos/mikes.json'); if (response.status !== 200) { throw new Error('could not fetch the data'); } const data = await response.json(); return data; };
Y ahora sí, en la consola podrás ver nuestro propio mensaje de error que sólo ocurrirá si hemos cometido un error escribiendo el endpoint. ?
? promise rejected: could not fetch the data
THE END!
¡Y con esto terminamos nuestra guía completa de JavaScript asíncrono! Espero que hayas aprendido algo nuevo ?. Si te queda alguna duda, ¡nos vemos en los comentarios!
Si quieres seguir practicando, no te pierdas este proyecto donde ponemos en práctica todo lo aprendido.
Y si crees que este post puede serle útil a alguien, ¡compártelo!
Otros artículos que pueden interesarte