Aprende a usar Promises y deja de usar callbacks
Table of Contents
Los Promises son una característica del Javascript moderno que nos permite realizar operaciones asíncronas de forma más legible y eficiente. Con estas podemos reemplazar los callbacks que se le pasan a estas operaciones y nos facilitan las cosas cuando tenemos que ejecutar varias una tras otra.
En este artículo te voy a enseñar las bases para que comiences a utilizar los Promises en lugar de callbacks y cómo esto va a ayudarte a escribir código más limpio.
Pero primero que nada ¿qué son las operaciones asíncronas?
Operaciones asíncronas
Cuando hablamos de operaciones asíncronas, nos referimos a aquellas que no bloquean el hilo principal de Javascript, como cuando se hace alguna petición a la red o de entrada y salida del disco duro.
Esto implica que el código no se ejecuta en secuencia (línea tras línea), sino que cuando el flujo de ejecución se encuentra con una operación asíncrona, no espera a que esta termine, sino que continúa corriendo el código que está más allá.
Para mostrarte de qué hablo, en el siguiente ejemplo vamos a cargar un archivo asíncronamente desde el disco duro con Node.js
const fs = require('fs');
fs.readFile('libros.txt', 'utf8', (error, libros) => {
if (error) throw error;
console.log(libros);
});
console.log('leyendo libros.txt');
Siguiendo el orden de las líneas:
- primero importamos el módulo
fs
de Node.js para acceder al sistema de archivos - Luego leemos el archivo
libros.txt
con el métodoreadFile()
. A este le pasamos un callback (el tercer parámetro, que es el arrow function) que va a imprimir el contenido del archivo por pantalla - Finalmente imprimimos "
leyendo libros.txt
"
Cuando ejecutamos este script, vemos que inmediatamente se imprime leyendo libro.txt
y luego el contenido del archivo libros.txt
.
¿No se suponía que la línea que imprime los libros por pantalla debería ejecutarse primero?
Nop, pues esta es una operación asíncrona y el código no se bloquea para esperar a que el programa lea el archivo desde el disco.
Este sigue corriendo y una vez tenga el archivo cargado, ejecuta las líneas dentro de la función callback. Si hubo un error leyendo libros.txt
(en caso de que el archivo no exista, por ejemplo), lanzamos un error. Si por el contrario todo salió bien, imprimimos el contenido del archivo.
Todo parece funcionar bien hasta aquí al usar callbacks para operaciones asíncronas.
Sin embargo vamos a comenzar a tener problemas de legibilidad en nuestro código cuando tengamos que realizar otras operaciones asíncronas una vez la primera se haya completado.
El problema con los callbacks
Digamos que vamos a imprimir por pantalla los archivos libros.txt
, autores.txt
y temas.txt
en ese orden.
Como fs.readFile()
funciona de forma asíncrona, no podremos simplemente usarlo tres veces para cargar archivo por archivo, una línea tras otra.
Para realizar esta tarea vamos a tener que:
- Usar el método
fs.readFile()
para leer del discolibros.txt
- Dentro del callback de este método, usamos otra vez
fs.readFile()
para leer del discoautores.txt
- dentro del callback de este último, usamos de nuevo
fs.readFile()
para leer del disco nuestro último archivotemas.txt
¿Viste qué loco y enredado resultó explicar eso? Vamos a ver cómo se implementa en código mejor:
const fs = require('fs');
let libros, autores, temas;
fs.readFile('libros.txt', 'utf8', (error, libros) => {
if (error) throw error;
libros = libros;
fs.readFile('autores.txt', 'utf8', (error, autores) => {
if (error) throw error;
autores = autores;
fs.readFile('temas.txt', 'utf8', (error, temas) => {
if (error) throw error;
temas = temas;
console.log('libros:', libros);
console.log('autores:', autores);
console.log('temas:', temas);
});
});
});
console.log('cargando archivos...');
Sin embargo, hay un problema con el uso de los callbacks: entre más procesos asíncronos tengamos, más niveles de anidamiento necesitaremos. En el ejemplo anterior tenemos solamente tres, pero...
¿Qué si por ejemplo tenemos diez archivos por cargar?
Tendríamos diez niveles de identación en nuestro código. Este se vería de forma piramidal. A esto es a lo que llaman pirámide de la muerte de los callbacks.
Además, este tipo de implementación impide que los procesos se realicen en paralelo, aumentando el tiempo de espera para la carga de los archivos.
Las promesas al rescate
Los Promises funcionan de forma similar a las promesas en la vida real. Estos nos prometen que vamos a obtener en el futuro un resultado de un proceso asíncrono, ya sea que este se realice satisfactoriamente o que falle.
Y nos garantizan que lo que pase solo va a ocurrir una vez.
Veamos un ejemplo de cómo se vería un script para la carga de un archivo usando Promises
:
cargarArchivo().then(archivo => {
console.log(archivo);
}).catch(error => {
console.log(error);
});
Si la función asíncrona cargarArchivo()
retorna un Promise, podemos adjuntar un then()
a esta para procesar su respuesta si la operación fue exitosa (es decir, si pudimos cargar el archivo con éxito).
Adjuntamos un catch()
al final del código para capturar el error y hacer algo con él en caso de que algo haya salido mal.
¿Y por qué es importante esto?
Los Promises nos evitan usar callbacks y tener que anidar uno dentro del otro para realizar varios procesos asíncronos que se necesitamos ejecutar en secuencia.
Cada then()
retorna a su vez un Promise al cual le podemos adjuntar otro then()
para procesar su respuesta.
Refactorizando callbacks en Promises
Veamos cómo podría verse el ejemplo anterior de la carga de tres archivos si se usaran Promises en lugar de callbacks:
let libros, autores, temas;
cargarLibros()
.then(books => {
libros = books;
return cargarAutores();
})
.then(authors => {
autores = authors;
return cargarTemas();
})
.then(topics => {
temas = topics;
console.log('libros:', libros);
console.log('autores:', autores);
console.log('temas:', temas);
})
.catch(error => {
console.log('hubo un error cargando algún archivo');
});
¿Mucho mejor, no?
Ya no tenemos que hacer anidaciones y terminar con una pirámide de la muerte de callbacks.
Para conseguir esto, las funciones cargarLibros()
, cargarAutores()
y cargarTemas()
deben retornar cada una un Promise.
Cuando la primera función termine de cargar libros , el then()
de la línea 4
se encarga de asignar el contenido del los libros a la variable libros
y retorna la siguiente función para cargar los autores.
Una vez tenemos cargado el archivo de autores, en el then()
de la línea 8
asignamos el resultado que retornó esta función a la variable autores
y retornamos función para cargar los temas.
Una vez se haya cargado este último archivo, en el último then()
(línea 12
) procedemos a imprimir las variables que fuimos asignando para libros, autores y temas.
El último método que adjuntamos a la cadena de promesas, catch()
, captura el error que haya ocurrido en cualquier eslabón de la cadena de Promises.
Lo que quiere decir que si la carga de cualquiera de los archivos falla, el error es capturado por este catch()
y desde allí lo podemos manejar (en nuestro caso, solo imprimimos en pantalla que hubo un error).
Implementando Promises desde cero
Ahora bien, ¿no es posible entonces adjuntar un then()
al método fs.readFile()
para usar Promises en lugar de callbacks? Por desgracia, no es así de fácil.
fs.readFile()
no retorna un Promise.
Sin embargo, podemos implementar una función que lea un archivo del disco duro alrededor de este método y que devuelva un Promise.
Así es como va a resultar la refactorización en Promises de nuestro ejemplo anterior de leer archivos del disco:
const fs = require('fs');
let libros, autores, temas;
// creamos una función asíncrona que retorne una Promise
function cargarArchivo(archivo) {
return new Promise((resolve, reject) => {
fs.readFile(archivo, 'utf8', (error, datos) => {
if (error) reject(error);
resolve(datos);
});
});
}
// ahora sí podemos usar .then() y .catch()
cargarArchivo('libros.txt')
.then(books => {
libros = books;
return cargarArchivo('autores.txt');
})
.then(authors => {
autores = authors;
return cargarArchivo('temas.txt');
})
.then(topics => {
temas = topics;
console.log('libros:', libros);
console.log('autores:', autores);
console.log('temas:', temas);
})
.catch(error => {
console.log('hubo un error cargando algún archivo')
}); // si ocurre algún error cargando cualquier archivo
El constructor de Promise recibe una función anónima con dos argumentos: resolve
y reject
.
El primero resuelve la promesa si su resultado fue exitoso y su resultado va a poder ser procesado por el método then()
. El segundo la rechaza si hubo un error. Este va a ser recolectado y procesado en el método catch()
.
Cómo resolver un array de Promises en paralelo
Uff hemos cubierto bastante tema hasta aquí.
Ya con esto tienes las bases para empezar a implementar Promises en Javascript. Sin embargo, quiero mostrarte una funcionalidad más que nos va ayudar a mejorar aún más nuestro anterior ejemplo.
Existe un método llamado Promise.all()
que permite resolver varias Promises en paralelo.
Veamos el siguiente código para ilustrar su uso:
// definición de la función cargarArchivo()
// y declaración de las variables
Promise.all([
cargarArchivo('libros.txt'),
cargarArchivo('autores.txt'),
cargarArchivo('temas.txt')
])
.then(archivos => { // retorna un array con las promesas resueltas
libros = archivos[0];
autores = archivos[1];
temas = archivos[2];
})
.catch(error => {
console.log('hubo un error cargando algún archivo')
});
Este método recibe un array con los Promises y los resuelve todos en paralelo. Luego retorna un array con la lista de los resultados una vez que todos las Promises hayan sido resueltos, en el orden. Si ocurre un error en cualquiera de estos, Promise.all()
retornar un error y este podrá capturado y manejado por catch()
.
Recuerda siempre es buena práctica que pongas un catch()
al final de una cadena de Promises para manejar los errores.
Espero que esta introducción (un poco larga) a los Promises como alternativa a los clásicos callbacks haya sido lo suficientemente clara y concisa.
Si quieres profundizar más sobre el funcionamiento de este patrón junto con sus especificaciones y usos, la documentación de Mozilla.org ofrece una buena guía al respecto. También la de Google developers.
Comments