Promesas, fetch, async y await

En este artículo veremos varios conceptos relacionados con la asincronía en Javascript.

Asincronía vs Sincronía

En programación, nos referimos a procesos síncronos a aquellos que se realizan de manera secuencial, uno detrás de otro, sin simultaneidad.

Los procesos asíncronos, sin embargo, se realizan en paralelo, todos al mismo tiempo.

Javascript es un lenguaje síncrono. Todo lo que hagas se ejecutará en el mismo hilo. Javascript tiene un único hilo, por lo tanto, no ofrece la posibilidad de simultaneidad en su ejecución.

Entonces… ¿cómo se realizan los procesos asíncronos en Javascript?

Asincronía en Javascript

Fíjate en el siguiente código. ¿Puedes predecir que se verá en la consola del navegador cuando lo ejecutes?

<script>

    console.log(1);
    setTimeout(function(){ console.log(2) }, 0);
    console.log(3);

</script>

La respuesta es: 132.

Para comprender la respuesta, debemos entender el siguiente gráfico:

Call Stack

Javascript tiene una pila de contextos que se van ejecutando (call stack). Cada función que ejecutamos tiene su propio contexto. Los contextos se van colocando uno tras otro en esta pila.

Web API

Cuando quieres lanzar un proceso asíncrono, este es gestionado por la API del navegador, que sí que funciona de manera asíncrona (aunque la especificación de ECMA script, no).

Callback Queue

Al terminar el proceso asíncrono de la Web API, pasa a ser gestionador por el callback queue, que espera a que el callstack no tenga ningún contexto en ejecución para ejecutarse

Explicación paso a paso

  1. Se ejecuta la línea 1, que hace un log del número 1.
  2. Se ejecuta la línea 2. En esta línea creamos un evento timer con la función setTimeout. Éste nos abriría un subproceso asíncrono donde se esperaría el tiempo especificado en la función (en este caso 0s) y al acabar éste se notificaría como terminado a la callback queue.
  3. Se ejecuta la línea 3, que hace un log del número 3.
  4. Finalmente al estar el stack vacío, pasamos a ejecutar los eventos guardados en la cola de eventos (callback queue). En este caso será el código registrado por el setTimeout de la línea 2, que hace un log del número 2.

¿Quieres verlo funcionando? Aquí tienes una escenificación de lo que está pasando.

Anonymous es la función anónima que estámos disparando en el setTimeout.

En el callback queue no todos los eventos tienen la misma prioridad. Por ejemplo, una promesa tiene más prioridad que un timeout.

¿Que es una promesa?

Una Promise es un objeto que nos permite manejar código asíncrono. Por ejemplo, una llamada al método fetch devuelve una Promise. Este objeto devuelto nos permite usar el método then. El método then, nos permite ejecutar un código cuando la promesa haya concluído.

Veamos un ejemplo de uso fetch:

<script>

    fetch('https://pokeapi.co/api/v2/pokemon/ditto/')
    .then( res => res.json() )
    .then( res => console.log(res));

</script>
  1. El código anterior hace una petición a una API pública de pokemons.
  2. Esta API devuelve una promesa. Cuando se haya cumplido llamaremos al método json() que devuelve otra promesa que convierte la respuesta de la petición ( texto plano ) en un objeto de Javascript.
  3. El segundo y último then procesa la promesa devuelta por el método .json() y hace un console.log para imprimirla en la consola.

Usando async y await

Vamos a realizar la misma petición del ejemplo anterior, pero ahora en lugar de utilizar la api de Promise (método then ) para procesar la respuesta vamos a utilizar los modificadores async y await.

<script>

    async function getPokemon() {
        const result = await fetch('https://pokeapi.co/api/v2/pokemon/ditto/');
        const res = await result.json();
        console.log(res);
    };
    getPokemon();

</script>
  • El modificador await siempre debe estar dentro de una función con el modificador async, o daría error. Este modificador hace que hasta que no termine la promesa que le hemos pasado, las siguientes líneas de código no se ejecuten.
  • El modificador async convierte la función en una Promise.

Anidación de promesas

Vamos ahora a hacer una petición a la API de Pokemon, y cuando hayamos recibido el valor, vamos a procesarlo y en función de este vamos a hacer otra petición a la API de Star Wars.

En nuestro ejemplo, consultaremos la altura del pokemon ditto, y en función de ella recuperaremos un personaje de Starwars.

<script>
    
    function getPokemons1() {
        fetch('https://pokeapi.co/api/v2/pokemon/ditto/')
        .then( res => res.json())
        .then( pokemon => {
            return fetch(`https://swapi.co/api/people/${pokemon.height}`)
            .then( res => res.json())
            .then( res => console.log(res));
        });
    }
    
    getPokemons1();

</script>

Cómo puedes ver en el código anterior, tenemos una anidación, en la que utilizamos la promesa que nos devuelve una primera petición fetch a la API de pokemon, para hacer otra petición fetch a la API e starwars.

Vamos a ver ahora como quedaría nuestro código si hubieramos usado asyncy await:

<script>

    async function getPokemons2() {
        const result = await fetch('https://pokeapi.co/api/v2/pokemon/ditto/');
        const pokemon = await result.json();
        const result2 = await fetch(`https://swapi.co/api/people/${pokemon.height}`);
        const b = await result2.json();
        console.log(b);
    };
    getPokemons2();

</script>

El primer código, en el que usábamos la API de Promise (fetch) y el segundo, en el que usamos los modificadores async y await, hacen lo mismo. Personalmente creo que el segundo, al evitar las anidaciones está un poco más claro, pero es cuestión de gustos.

Gestión de errores

En el siguiente código tenemos una función getPromiseQueAVecesFalla() que devolverá una promesa. Dentro de esta función hemos puesto un random, para que a veces la promesa devuelva correcto y otras incorrecto. Hacemos esto para tratar de emular lo que pasaría en el servidor en un caso real en el que estamos pasando mal los parámetros, por ejemplo.

  • resolve indica que la promesa ha ido bien.
  • reject indica que la promesa ha tenido errores.

El método handlePromise1() gestionará la promesa que devuelve getPokemon() utilizando la API de Promise. Como puedes ver en este caso, seguimos utilizando el método then para gestionar lo que ocurre si la promesa va bien, pero ahora además usaremos el método catch para gestionar lo que ocurre en caso de error.

El método handlePromise2() gestionará la promesa que devuelve getPokemon() utilizando async y await. En este caso, el código que es susceptible de arrojar una excepción será encapsulado en una estructura de tipo try, y la gestión del error, si sucediese, sería realizada en una estructura de tipo catch.

<script>
    
    function getPromiseQueAVecesFalla() {
        return new Promise((resolve, reject) => {
            if (Math.random() > 0.5) {
                reject('Ha habido un error'); 	
            } else {
                resolve({ value: "everything is allright" })
            }
        });
    }

    function handlePromise1() {
        return getPromiseQueAVecesFalla()
            .then(value => console.log(value))
            .catch(error => console.log(error));
    };

    async function handlePromise2() {
        try {
            const value = await getPromiseQueAVecesFalla();
            console.log(value);
        } catch (error) {
            console.log(error)
        }
    };

</script>

Conclusion

Puedes gestionar una petición ajax, tanto usando la API de Promise (que es lo que te devuelve un fetch) como usando async-await, no obstante, intentaremos mantener siempre nuestro código lo más limpio y claro posible.

Si tienes que gestionar varios tipos de errores o recoger lo que te devuelve una petición para hacer otra, es posible que async-await te permita tener un código mas claro al evitar la anidación. En caso contrario, usar la API de Promise puede ser la mejor opción.

Aviso Legal | Política de privacidad