Saltar al contenido principal

Programación asincrónica

JavaScript es un lenguaje no bloqueante, lo que significa que cualquier acceso a dispositivos de E/S (Entrada y Salida) como el disco duro, la red, etc., ocurre de forma asincrónica.

Asincrónico significa que no vamos a esperar una respuesta inmediatamente, sino que en algún momento en el futuro vamos a recibir la respuesta. Puedes compararlo con en el correo electrónico, que nos permite comunicarnos de forma asincrónica, cuando enviamos un correo a otra persona no esperamos la respuesta inmediatamente, podemos hacer otras actividades mientras recibimos la respuesta.

En JavaScript existen varias formas de manejar la programación asincrónica. En este capítulo vamos a ver tres: callbacks, promesas y la nueva sintaxis de async/await.

Callbacks

Un callback es una función que se le pasa como parámetro a otra función. Por ejemplo, el siguiente código utiliza jQuery para hacer un llamado AJAX e imprimir el resultado en la consola:

$.get("http://example.com/", function(data) {
console.log(data);
});

console.log("Esto se imprime primero");

Lo más importante de este ejemplo es entender que el método get está recibiendo dos parámetros: una URL y un callback.

La forma en que JavaScript ejecuta este código es la siguiente:

  1. JavaScript realiza la petición HTTP a http://example.com/ de forma asincrónica.
  2. El programa continúa su ejecución (no se bloquea) e imprime "Esto se imprime primero" en la consola.
  3. Cuando el get recibe la respuesta del servidor invoca el callback y se imprime el resultado en la consola.

El problema con los callbacks es que cuando se anidan muchos llamados asincrónicos se crea lo que se conoce como el callback hell, que son muchos callbacks anidados. El siguiente código ficticio demuestra el problema:

$.get("http://example.com/", function(data) {
db.find(data, function(result) {
file.read(result, function(content) {
file.save(content, function() {
// y así sucesivamente
});
});
});
});

Para que el código sea vea más secuencial se introdujeron las promesas en ES6.

Promesas

Las promesas están construídas sobre callbacks pero hacen que el código se vea mucho más secuencial:

algoAsincronico()
.then(actualizarAlgo)
.then(guardarAlgo)

jQuery ahora soporta promesas para hacer llamados AJAX, así que podemos modificar nuestro ejemplo ficticio anterior de la siguiente forma:

$.get("http://example.com/")
.then(function(data) { return db.find(data); })
.then(function(result) { return file.read(result); })
.then(function(content) { return file.save(save); });

Si utilizamos las funciones flecha de ES6 el código se ve aún más compacto:

$.get("http://example.com/")
.then(data => db.find(data))
.then(result => file.read(result))
.then(content => file.save(save));

El método then recibe dos parámetros: un callback de éxito y otro cuando ocurre un error. Sin embargo, las promesas también tienen un método catch que se puede utilizar para manejar el error que se produzca en cualquier then:

$.get("http://example.com/")
.then(data => db.find(data))
.then(result => file.read(result))
.then(content => file.save(save))
.catch(error => console.log("Ocurrió un error"))

Algo que estamos asumiendo en este ejemplo es que todos llamados db.find, file.read y file.save retornan una promesa (de lo contrario la cadena se quiebra). Pero supongamos que db.find no retorna una promesa, sólo acepta un callback, en ese caso nos tocaría crear una promesa.

Creando promesas

Las promesas se crean con el objeto Promise, que recibe un callback con dos parámetros:

new Promise(function(resolve, reject) {
// invoca resolve() para resolver la promesa o reject() para generar un error
});

Por ejemplo, vamos a crear un método wait que envuelva setTimeout (que no soporta promesas) en una promesa y la devuelva:

function wait(timeInMillis) {
return new Promise(function(resolve) {
setTimeout(function() { resolve(); }, timeInMillis);
});
}

// ahora utilicemos nuestro método wait
wait(1000)
.then(function() { console.log("Hola después de 1 segundo"); });

Volviendo a nuestro ejemplo ficticio, imagina que db.find no soporta promesas, igual podemos envolverlo en una promesa:

$.get("http://example.com/")
.then(data => {
return new Promise(resolve => {
db.find(data, result => resolve(result))
});
})
.then(result => file.read(result))
.then(content => file.save(save))
.catch(error => console.log("Ocurrió un error"))

Fíjate cómo utilizamos resolve para pasar el resultado a la siguiente promesa. Esto es un patrón muy común en desarrollo con JavaScript hoy en día.

El problema de las promesas es que, como te puedes dar cuenta, no son triviales y son muy sujetas a errores. Por ejemplo, si en algún punto de la cadena de then no devuelves una promesa, la cadena se rompe y encontrar ese tipo de errores es difícil.

Async/await

Continuando con la tradición de intentar que el código asincrónico de JavaScript se vea más sincrónico, en ES7 se introdujo async/await, que Node.js soporta completamente en las versiones recientes.

Async/await está construido sobre promesas.

Veamos un ejemplo:

const performHttpRequest = async () => {
const result = await http.get();
console.log(result);
}

performHttpRequest();