En este artículo explicaremos que es el Callback Hell y cómo solucionarlo mediante Promesas y en conjunto con Async/Await. Veremos algunos ejemplos que nos ayudaran a entender estas herramientas que nos provee JavaScript para trabajar con peticiones asíncronas.
Para entender la solución que nos brinda el uso de las Promesas, primero debemos conocer cuál es la problemática: Callback Hell 🔥.
Callback Hell
Callback Hell o “Pirámide de la perdición” se le llama comúnmente al código que se produce cuando encadenamos muchas operaciones asíncronas seguidas, formando una especie de “pirámide” que va creciendo conforme agreguemos más operaciones.
En este articulo exploraremos cómo se produce un callback hell con un ejemplo sencillo de cómo unas operaciones asíncronas pueden escalar hasta convertir nuestro código en un callback hell.
Imaginemos que tenemos un carrito de compras al cual le vamos agregando productos mediante una función.
let compras = "🛒";
function hacerLaCompra(){
compras += "📦";
}
hacerLaCompra();
console.log(compras); // 🛒📦
Hasta ahora no hay nada de especial, creamos una función hacerLaCompra()
y ésta agrega al carro un producto que luego mostramos por consola, pero para imitar una petición a una API vamos a forzar la asincronía mediante un setTimeout()
. (Para saber más sobre asincronía ver este Blog)
let compras = "🛒";
function hacerLaCompra(){
setTimeout(function(){
compras += "📦";
}, 2000);
}
hacerLaCompra();
console.log(compras); // 🛒
Ahora que la función es asincrónica, la consola nos devuelve solo el carrito sin productos cargados, como si nunca llegara a ejecutar lo que está dentro del setTimeout()
. Por qué pasa esto? Bien, para entender lo que está pasando debemos saber que JavaScript es un lenguaje de un solo hilo, es decir, que puede ejecutar solo una cosa a la vez, para manejar la asincronía JavaScript usa un modelo asincrónico no bloqueante que nos permite realizar operaciones asincrónicas sin bloquear la ejecución del programa.
Esto significa que el programa continúa ejecutándose independientemente de si el setTimeout()
terminó de ejecutarse.
Pero que ocurre si necesito esperar el resultado del setTimeout()
? Ahí es donde entran los callbacks.
Un callback es una función pasada como parámetro a otra función, en nuestro caso lo vamos a usar para ejecutar esa función después de que termine el setTimeout()
.
let compras = "🛒";
function hacerLaCompra(proximaFuncion){
setTimeout(function(){
compras += "📦";
proximaFuncion();
}, 2000);
}
hacerLaCompra(function(){
console.log(compras); // 🛒📦
})
Ahora sí vemos por consola que se agregó el producto, por si no queda del todo claro lo que ocurre aquí, es lo siguiente:
- Llamamos a la función
hacerLaCompra()
pasándole por parámetro una función. - Se espera los 2 segundos del
setTimeout()
. - Se agrega a compras el “📦”.
- Se ejecuta la función del parámetro
"proximaFuncion” → function(){console.log(compras);}
Pero dónde está el “Hell” antes mencionado? Bien, si quisiéramos comprar varias cosas tendríamos que anidar la llamada de las funciones, esto se vería de la siguiente manera:
hacerLaCompra(function(){
console.log(compras); // 🛒📦
hacerLaCompra(function(){
console.log(compras); // 🛒📦📦
hacerLaCompra(function(){
console.log(compras); // 🛒📦📦📦
hacerLaCompra(function(){
console.log(compras); // 🛒📦📦📦📦
hacerLaCompra(function(){
console.log(compras); // 🛒📦📦📦📦📦
})
})
})
})
})
Esto es lo que se llama Callback Hell, que quizás a simple vista no se vea como un problema porque a este ejemplo le falta el manejo de posibles errores, que generalmente se le agrega a cada llamada para reaccionar ante posibles fallos… no abordaremos eso en este blog, pero imaginemos que en cada llamada tengamos, por ejemplo, un if
consultando si se agregó el producto para avanzar o hacer otra cosa mediante el else
…
Ante esta problemática surgió lo llamado “Promises
” que vino para tratar con las operaciones asíncronas de una manera más elegante.
Promesas
Las promesas o promises sirven para trabajar con peticiones asíncronas, por lo tanto, es utilizado para disminuir el uso excesivo de callbacks.
Una promesa en JavaScript podría ser comparada con una promesa en la vida real, si haces una promesa pueden pasar 2 cosas, una es que se cumpla y otra es que no. En JavaScript es lo mismo, una promesa tiene dos “estados” llamados generalmente resolve
y reject
.
Cuando creamos una promesa esta recibe como parámetro una función y esta función recibe dos parámetros que generalmente son definidos como resolve
y reject
los cuales son ejecutados si se cumple o no la promesa. Dentro de función definiremos la condición para que se complete la promesa por alguno de los dos caminos:
let promesa = new Promise(function(resolve, reject) => {
let aleatorio = Math.random();
setTimeout(function() => {
if (aleatorio < 0.5) {
resolve("📦");
} else {
reject("Hubo un error");
}
}, 2000);
});
En este ejemplo creamos la promesa que, aleatoriamente, nos devuelve uno de los dos caminos (resolve
y reject
).
Nota: Una vez que la promesa llega a un resolve
o reject
esta retorna su contenido y termina la ejecución de la promesa, por lo que bajo ninguna circunstancia puede retornar más de un estado.
Una vez creada la promesa debemos ejecutarla haciendo uso del método .then()
que recibe lo que es enviado por el resolve
, y el método .catch()
que recibe lo que sea enviado por el reject
.
También hay un método más, el finally()
, que es opcional y que se ejecuta SIEMPRE, sin importar si la promesa fue satisfactoria o rechazada.
promesa
.then((resultado) => console.log(resultado))
.catch((error) => console.log(error))
.finally(() => console.log("Promesa finalizada"))
// 📦
// Promesa finalizada
Pero en este ejemplo solo la ejecutamos una vez, para ver como las promesas vienen a subsanar el problema del callback hell debemos anidar las promesas de la siguiente manera:
let compras = "🛒";
function hacerLaCompra() {
return new Promise((resolve) => {
setTimeout(() => {
compras += "📦";
console.log(compras);
resolve();
}, 2000);
});
};
hacerLaCompra()
.then(() => hacerLaCompra())
.then(() => hacerLaCompra())
.then(() => hacerLaCompra())
.then(() => hacerLaCompra())
.finally(() => console.log("Finalizó la compra"));
Esta funcion tiene unos cambios con respecto a las anteriores, aca la salida por consola se encuentra dentro del setTimeout()
, y vemos que el .then()
ejecuta nuevamente el metodo hacerLaCompra()
por lo que el proximo .then()
recibe esa respuesta (que es nada) y vuelve a llamar al metodo hacerLaCompra()
, dando como resultado lo siguiente:
// 🛒📦
// 🛒📦📦
// 🛒📦📦📦
// 🛒📦📦📦📦
// 🛒📦📦📦📦📦
// Finalizó la compra
Como vemos, es mucho más sencillo ejecutar peticiones anidadas de esta forma, pudiendo tener opcionalmente un solo .catch al final para capturar cualquier error que sea lanzado en cualquiera de las peticiones.
Pero… todavía hay una manera más simple de realizar peticiones, esto es con el Async y Await.
Async/Await
En EcmaScript 2017 se introdujo las palabras clave async
/await
, que no son más que “azúcar sintáctico” para gestionar las promesas de una forma más sencilla.
En primer lugar, tenemos la palabra clave async
que se coloca previamente a una function, para definirla como una función asincrónica:
async function hacerLaCompra(){ return "📦"; }; hacerLaCompra(); // Promise {: '📦'}
Al ejecutar la función veremos que nos devuelve una promise que ha sido satisfactoria, con el valor devuelto por la función (en este caso “📦”). De hecho, podemos usar .then()
para manejar la respuesta.
hacerLaCompra() .then(valor => console.log("El resultado devuelto es: " + valor)); // El resultado devuelto es: 📦
Sin embargo, cuando usamos un método async
suele usarse con la palabra clave await
, que es donde reside lo interesante de utilizar este enfoque.
Cualquier función definida con Async
, o cualquier promesa, puede ser utilizada junto con la palabra clave await
. Lo que hace await
es esperar que se resuelva la Promise, mientras permite que se ejecuten otras tareas en el proceso.
const resultado = await hacerLaCompra(); console.log(resultado) // 📦
Esto hace que la forma de trabajar con async
/await
sea más mucho más fácil para aquellos que no estén acostumbrados a las promesas y a la asincronía en general, ya que el código parece síncrono.
Para afianzar estos conceptos veremos un ejemplo usando Promise
y Async
/Await
en conjunto
function elevarAlCuadradoPromesa(numero) { return new Promise((resolve,reject) => { if(isNaN(numero)){ reject("Error, el valor '" + numero + "' ingresado no es un número"); } setTimeout(() => { resolve({ numero, resultado: numero * numero }); }, 2000); }); } async function funcionAsincronica() { try { let objeto = await elevarAlCuadradoPromesa(2); console.log("Número: " + objeto.numero + ", resultado: " + objeto.resultado); objeto = await elevarAlCuadradoPromesa("texto"); console.log("Número: " + objeto.numero + ", resultado: " + objeto.resultado); }catch(error){ console.log(error); } } funcionAsincronica();
En este ejemplo tenemos una función que retorna una promesa, en caso de enviarle por parámetros un valor que no sea un numero esta lanzara un error con un mensaje. Por otro lado tenemos la función asíncrona que “consume” la promesa. El resultado de este ejemplo se vería así:
// Número: 2, resultado: 4 // Error, el valor 'texto' ingresado no es un número