Original article: Asynchronous JavaScript – Callbacks, Promises, and Async/Await Explained

Si has estado aprendiendo JavaScript por un tiempo ahora, entonces probablemente has oído del término "asincronía" antes.

Esto es porque JavaScript es un lenguaje asíncrono... pero ¿qué significa realmente eso? En este artículo, espero mostrarte que el concepto no es tan difícil como suena.

Sincronía vs. asincronía

Antes de saltar al tema en sí, miremos a estas dos palabras – sincronía y asincronía.

Por defecto, JavaScript es un lenguaje de programación síncrono de un solo hilo. Esto significa que las instrucciones pueden solamente ejecutarse una después de la otra, y no en paralelo. Considera el pequeño fragmento de código debajo:

let a = 1;
let b = 2;
let suma = a + b;
console.log(suma);

El código de arriba es bastante sencillo – suma dos números y luego muestra la suma en la consola del Navegador. El interpretador ejecuta estas instrucciones una después de la otra en ese orden hasta que termine.

Pero este método viene con desventajas. Digamos que queríamos buscar alguna cantidad grande de datos de una Base de Datos y luego mostrarlo en nuestra interfaz. Cuando el interpretador alcance la instrucción que busca estos datos, el resto del código se bloquea al ejecutar hasta que los datos hayan sido buscados y devueltos.

Ahora podrías decir que los datos a ser buscados no son tan grandes y no tomará ningún tiempo notable. Imagina que tienes que buscar datos en múltiples puntos distintos. Este retraso compuesto no suena como algo que los usuarios les gustaría encontrarse.

Afortunadamente, para nosotros, los problemas con JavaScript síncrono fueron abarcados al introducir JavaScript asíncrono.

Imagina el código asíncrono como código que comienza ahora, y termina su ejecución después. JavaScript está ejecutando asincrónicamente, las instrucciones no son necesariamente ejecutadas una después de la otra como vimos antes.

Para implementar este comportamiento asíncrono apropiadamente, hay unas pocas soluciones distintas que los desarrolladores han usado a lo largo de los años. Cada solución es mejor que la otra, lo cual hace al código más óptimo y más fácil de entender en el caso de que se vuelva complejo.

Para entender más allá la naturaleza asíncrona de JavaScript, veremos las funciones callback, promesas, y async y await.

¿Qué son los callbacks en JavaScript?

Un callback es una función que se pasa dentro de otra función, y luego se lo llama dentro de esa función para realizar una tarea.

¿Confuso? Vamos a desglosarlo implementándolo en la práctica.

console.log('aparece primero');
console.log('aparece segundo');

setTimeout(()=>{
    console.log('aparece tercero');
},2000);

console.log('aparece ultimo');

El fragmento de arriba es un pequeño programa que imprime cosas a la consola. Pero hay algo nuevo aquí. El interpretador ejecutará la primera instrucción, luego la segunda, pero salteará la tercera y ejecutará la última.

El setTimeout es una función de JavaScript que toma dos parámetros. El primer parámetro es otra función, y el segundo es el tiempo después el cual esa función debería ser ejecutada en milisegundos. Ahora ves la definición de callbacks entrando en juego.

La función dentro de setTimeout en este caso es requerido para ejecutarse después de dos segundos (2000 milisegundos). Imagina que es tomado para ser ejecutado en alguna parte separada del navegador, mientras que las otras instrucciones continúan ejecutándose. Después de dos segundos, los resultados de la función son devueltos.

Por eso, si ejecutamos el fragmento de arriba en nuestro programa, obtendremos esto:

aparece primero
aparece segundo
aparece ultimo
aparece tercero

Ves que la última instrucción es impresa antes de que la función en el setTimeout regrese su resultado. Digamos que usamos este método para buscar datos de una base de datos. Mientras que el usuario está esperando que la llamada a la base de datos regrese los resultados, el flujo de ejecución no será interrumpido.

Este método era muy eficiente, pero solamente hasta un cierto punto. A veces, los desarrolladores tienen que hacer múltiples llamadas a diferentes recursos en sus códigos. Para hacer estas llamadas, los callbacks están siendo anidados hasta que se vuelven difícil de leer o de mantener. A esto se lo conoce como Callback Hell.

Para arreglar este problema, se introdujeron las promesas.

¿Qué son las promesas en JavaScript?

Escuchamos que las personas hacen promesas todo el tiempo. Ese primo tuyo que te prometió enviarte plata, un niño prometiendo no tocar el jarrón de galletitas de vuelta sin permiso... pero las promesas en JavaScript son un poco distinto.

Una promesa, en nuestro contexto, es algo que tomará algún tiempo en hacerse. Hay dos resultados posibles de una promesa:

  • O ejecutamos y resolvemos la promesa, o
  • Algún error ocurre a lo largo de la línea y la promesa es rechazada

Las promesas vinieron para resolver los problemas de las funciones callback. Una promesa toma dos funciones como parámetros. Eso es, resolve y reject. Recuerda que resolve es éxito, y reject es para cuando un error ocurre.

Miremos a las promesas trabajando:

const obtenerDatos = (dataEndpoint) => {
   return new Promise ((resolve, reject) => {
     //alguna peticion al endpoint;
     
     if(peticion es exitosa){
       //hace algo;
       resolve();
     }
     else if(hay un error){
       reject();
     }
   
   });
};

El código de arriba es una promesa, encerrado por una petición a algún endpoint. La promesa toma resolve y reject como mencioné antes.

Después de hacer una llamada al endpoint por ejemplo, si la petición es exitosa, resolveríamos la promesa y continuaríamos haciendo lo que quisiéramos con la respuesta. Pero si hay un error, la promesa será rechazada.

Las promesas son una forma limpia de arreglar los problemas provocados por el callback hell, en un método conocido como encadenamiento de promesas. Puedes usar este método para obtener datos secuencialmente de múltiples endpoints, pero con menos código y métodos más fáciles.

Pero, ¡hay una mejor forma inclusive! Podrías estar familiarizado con el siguiente método, ya que es una forma preferida de manejar los datos y las llamadas API en JavaScript.

¿Qué es async y await en JavaScript?

La cosa es, el encadenamiento de promesas juntos, justo como los callbacks se pueden poner bastante voluminoso y confuso. Por eso se produjo async y await.

Para definir una función async, haces esto:

const funcAsync = async() => {

}

Nota que llamando una función async siempre regresará una Promesa. Mira esto:

const prueba = funcAsync();
console.log(prueba);

Ejecutando lo de arriba en la consola del navegador, vemos que el funcAsync regresa una promesa.

Desglosemos algo de código ahora. Considera el pequeño fragmento de abajo:

const funcAsync = async () => {
	const respuesta = await fetch(recurso);
   	const datos = await respuesta.json();
}

La palabra clave async es lo que usamos para definir las funciones asíncronas, como mencioné arriba. Pero, ¿qué hay de await? Bueno, impide a JavaScript de asignar fetch a la variable respuesta hasta que la promesa se haya resuelto. Una vez que la promesa ha sido resuelta, los resultados del método fetch pueden ahora ser asignados a la variable respuesta.

Lo mismo sucede en la línea 3. El método .json regresa una promesa, y podemos usar await todavía para retrasar la asignación hasta que la promesa es resuelta.

Bloquear código o no bloquear Código

Cuando digo 'impedir', debes de pensar que implementar async y await de alguna manera bloquea la ejecución de código. Porque, ¿y qué si nuestra petición tarda demasiado, no?

El hecho es que no lo hace. El código que está dentro de la función async está bloqueando, pero eso no afecta la ejecución del programa de ninguna manera. La ejecución de nuestro código sigue siendo asíncrono como siempre. Para mostrar esto:

const asyncFunc = async () => {
  const respuesta = await fetch(recurso);
  const datos = await respuesta.json();
}

console.log(1);
cosole.log(2);

asyncFunc().then(datos => console.log(datos));

console.log(3);
console.log(4);

En nuestra consola de navegador, la salida de arriba luciría algo así:

1
2
3
4
datos regresados por asyncFunc

Ves que desde que llamamos a asyncFunc, nuestro código continuó ejecutándose hasta que fue el tiempo para la función regresar los resultados.

Conclusión

Este artículo no trata estos conceptos en gran profundidad, pero espero que te muestre lo que el JavaScript asíncrono implica y de tener cuidado de unas pocas cosas.

Es una parte muy esencial de JavaScript, y este artículo solamente raspa la superficie. Sin embargo, espero que este artículo haya ayudado en desglosar estos conceptos.