Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:programacion_avanzada_javascript:funciones_asincronas

¡Esta es una revisión vieja del documento!


Funciones asíncronas

Módulo perteneciente al curso Programación avanzada con JavaScript y ECMAScript.

Introducción

La programación asíncrona es un concepto siempre difícil. Hasta la aparición de las promises no teníamos en JavaScript un mecanismo para declarar asincronismo. Es cierto que disponíamos de un conjunto de APIs asíncronas (como setTimeout o XMLHttpRequest por citar dos), pero no teníamos manera de declarar las nuestras propias.

En el módulo de promises hemos aprendido a declarar funciones asíncronas devolviendo una promise y a esperar por ellas usando then. Pero las promises son relativamente complejas de utilizar y por ello se añadió async/await al lenguaje.

Es importante entender que async/await no habilita ningún escenario nuevo que no se pueda llevar a cabo mediante promises. De hecho async/await está construido sobre las promises, que son el verdadero mecanismo de asincronía en JavaScript. Es por ello que, a pesar de que quizá termines usando básicamente async/await para consumir código asíncrono debes entender cómo funcionan las promises, ya que son lo que hay realmente por debajo.

En este módulo aprenderemos a crear funciones asíncronas usando la sintaxis de async/await y veremos que, como decimos, por debajo, en realidad lo que hay son promises. Es por ello que es importante que todos los conceptos explicados en el módulo de promises los tengas claros antes de abordar este. Repásalos si lo consideras necesario.

¡Allá vamos!

¿Qué son las funciones asíncronas?

Una función asíncrona es aquélla que se ejecuta asíncronamente y que utiliza implícitamente una promise para devolver su valor. Las funciones asíncronas se definen exactamente igual que las funciones normales salvo que usamos la palabra clave async delante para denotar su naturaleza:

async function answerAsync() {
    console.log('answer is ' + 42)
}

Dado que las funciones asíncronas son también simples funciones, aplicar typeof a una función asíncrona devuelve “function”.

Las funciones asíncronas se invocan exactamente igual que las funciones tradicionales. Es decir, la función anterior la podemos invocar usando simplemente answerAsync().

invocación función asíncrona

Si nos fijamos en la imagen anterior, observamos cómo al invocar la función answerAsync el resultado devuelto no ha sido undefined, que es lo que devolvería la versión síncrona/convencional de esa misma función (recuerda que las funciones que no devuelven nada explícitamente devuelven undefined), sino que la función devuelve una promise. En este caso la promise está resuelta al terminar la llamada.

Esto nos permite, por supuesto, aplicar todo lo que conocemos de promises a funciones asíncronas:

answerAsync().then(() => console.log('Answer is written in the console'))

En este caso, una vez la función asíncrona se ha ejecutado, se ejecuta la función indicada dentro del then:

invocación función asíncrona y then

Por lo tanto toda función async devuelve una promise. El resultado devuelto por la función es el resultado que se resuelve con la promise.

En nuestro ejemplo answerAsync no devolvía nada, por lo que devolvía una promise que se resolvía sin valor (en cierta literatura se refiere a dichas promises como Promises Void o Promise<Void>), pero obviamente podemos devolver un valor:

async function answerAsync() {
    return 42;
}
 
answerAsync().then(v => console.log("Value is " + v));

Este código imprime Value is 42 por la consola, ya que la promise devuelta por answerAsync se resuelve con el valor 42.

Recuerda: una función async siempre devuelve una promise.

Esperas asíncronas

El ejemplo que hemos visto no es muy representativo porque, a pesar de que hemos creado una función asíncrona (con async), ésta se termina ejecutando síncronamente, ya que no hace nada realmente de forma asíncrona.

Si trasladamos el ejemplo anterior a promises sería como si tuviéramos lo siguiente:

function answerAsync() {
   const pr = new Promise((resolve, reject) => {
        console.log('answer is 42')
        resolve()
   });
   return pr;
}

Quizá ahora se observa mejor cómo la función answerAsync no ejecuta nunca nada de forma asíncrona: realiza un console.log (operación que es síncrona) y luego ya devuelve la promise resuelta.

Lo interesante de las funciones asíncronas es que pueden realizar esperas asíncronas: una espera asíncrona consiste en que la función asíncrona se espera hasta que una promise termina. Pero esa espera no es bloqueante, es decir, no bloquea el hilo principal de ejecución, ni por lo tanto, tampoco la página ni la interfaz de usuario.

¿Y qué es lo que podemos esperar asíncronamente? Pues dos cosas:

  • La resolución de una promise
  • Dado que las funciones asíncronas devuelven una promise, podemos esperar asíncronamente la finalización de una función asíncrona

Para esperas asíncronas usamos la palabra clave await. Vamos a ver un ejemplo para que quede claro.

DEMO: De promise a async

Veamos la relación entre async/await y las promises. En el fondo, async/await es syntactic sugar, una forma más sencilla de utilizar promises.

Partamos de la siguiente función que devolverá una promise que se resolverá al cabo de los segundos que pasemos por parámetro:

function waitPromise(sec) {
    const pr = new Promise((res, rej) => {
        window.setTimeout(res, sec * 1000);
    });
 
    return pr;
}

Probamos:

console.log("Incio espera");
 
waitPromise(3).then(() => {
    console.log("espera terminada");
});
 
console.log("fin");

Ahora haremos lo mismo usando async / await:

console.log("Incio espera");
 
async function waitAsync() {
    console.log("Dentro de waitAsync, pero antes del await");
    await waitPromise(3);
    console.log("espera terminada");
}
 
waitAsync();
 
console.log("fin");

await solo se puede utilizar dentro de un contexto async, por eso tuvimos que “envolver” await en una función que usa async.

La salida por consola será:

Inicio espera
dentro de waitAsync, pero antes del await
fin
espera terminada

Un "ping" asíncrono

Vamos a ver un ejemplo: crearemos una función que cargue asíncronamente una página web y que nos devuelva el tiempo (en milisegundos) que ha tardado. Una especie de “ping” a la página.

Observa que se trata de una función normal y corriente que devuelve una promise:

function ping(url) {
    const pr = new Promise((resolve, reject) => {
        const req = new XMLHttpRequest();
        const init = performance.now();
        req.open('GET', url, true);
        req.onreadystatechange = function(){
            if (req.readyState == XMLHttpRequest.DONE) {
                if (req.status == 200) {
                    const end = performance.now();
                    resolve(end-init);
                }
                else {
                    reject(-1);
                }
            }
        }
        req.send();
    })
    return pr;
}

Nota: observa el uso de performance.now(), que te permite obtener el tiempo desde que se cargó el documento. Usando dos valores obtenidos por perfomance.now() y restándolos, obtenemos el tiempo transcurrido entre dos instantes. Consulta la página de la MDN para más información.

Por supuesto, dado que dicha función devuelve una promise podemos usar then:

ping('https://www.google.es')
    .then(r => console.log(`ha tardado ${r}`))
    .catch(() => console.log('ups!!'));

Hasta este punto no hemos visto nada nuevo. Ahora vamos a introducir una espera asíncrona, es decir, vamos a usar await para esperar el resultado de ping:

const test = async() => {
    let ms = await ping('https://reqres.in/api/users');
    console.log("Time: " + ms);
}
 
test();

Fíjate en un par de cuestiones importantes:

  • El uso de await para esperar por la promise (devuelta por ping). Observa también cómo await nos devuelve ya el valor resuelto de la promise.
  • Para usar await debemos estar en una función asíncrona, es decir, declarada como async. Solo las funciones asíncronas (declaradas con async) pueden efectuar esperas asíncronas.

Y recuerda: el hecho de que test sea una función asíncrona, implica que podemos esperar por ella:

const test = async(uri) => {
    let ms = await ping(uri);
    console.log("Time: " + ms);
}
 
const testm = async() => {
    await test('https://reqres.in/api/users');
    await test('https://reqres.in/api/users');
}
 
testm();

Por supuesto, para que la función testm pueda usar await (y esperar asíncronamente por la función test), ella misma debe ser declarada con async.

Observa cómo el uso de async/await nos facilita el consumo de funciones que devuelven o esperan por promises, pero usar async no convierte una función en “asíncrona” de verdad (en el sentido de que se ejecute concurrentemente o en otro hilo). Usar async en una función f lo que hace es habilitar el uso de await en dicha función y permitir que otras funciones (que a su vez serán async) usen await para esperar por f. Pero en JavaScript lo que habilita el asincronismo de verdad son las promises (o el uso de otras APIs asíncronas anteriores a las promises tales como XMLHttpRequest o setInterval entre otras).

DEMO: Más sobre async y promises

Vamos a realizar un experimento que consiste en capturar el resultado de la función devuelta por waitAsync():

function waitPromise(sec) {
    const pr = new Promise((res, rej) => {
        window.setTimeout(res, sec * 1000);
    });
 
    return pr;
}
 
console.log("Incio espera");
 
async function waitAsync() {
    console.log("Dentro de waitAsync, pero antes del await");
    await waitPromise(3);
    console.log("espera terminada");
}
 
let resultado = waitAsync();
console.log("resultado", resultado);

Si waitAsync() fuese una función tradicional, el valor de la variable resultado sería undefined. Sin embargo, obtenemos una Promise.

Por eso podemos escribir:

let resultado = waitAsync();
 
resultado.then(() => {
    console.log("Función waitAsync() terminada");
});

Pongamos ahora el ejemplo de que la promesa devuelve un valor:

async function waitAsync() {
    console.log("Dentro de waitAsync, pero antes del await");
    await waitPromise(3);
    console.log("espera terminada");
    return 42;
}
 
let resultado = waitAsync();
 
resultado.then((v) => {
    console.log("Función waitAsync() terminada con valor: " + v);
});

Lo que hicimos con el then anterior, podríamos hacerlo de esta otra manera utilizando await:

async function f2() {
    let resultado = await waitAsync();
    console.log("resultado", resultado);
}
 
f2(); // -> 'resultado 42'

await nos recoge el resultado que está dentro de la promise y simplifica el código (no es necesario el uso de then).

Recapitulado:

  • await permite hacer esperas asíncronas sobre promises.
  • Para utilizar await necesitamos una función asíncrona marcada con async.
  • Las funciones marcadas con async devuelven una promise, por lo tanto podemos usar await para esperar sobre una función asíncronas (del mismo modo que usamos await para esperar por una promise).
  • Cuando una función asíncrona devuelve un resultado, await nos devuelve directamente el resultado y no la promise.

Errores en funciones asíncronas

Una de las facilidades de usar async/await es que permite mediante try/catch capturar tanto errores en funciones síncronas como en funciones asíncronas. Usando promises eso no es posible:

function asyncPromise() {
    return new Promise((resolve, reject) => {
        reject("Unspecified error")
    });
}
 
try {
    asyncPromise().then(() => {
        console.log('asyncPromise run!');
    });
}
catch (error) {
    console.err('asyncPromise failed with: ' + error);
}

Usando try/catch no se capturan las promesas rechazadas. Así, a pesar de que asyncPromise ha fallado, nosotros no capturamos el error:

Error en promise no capturado

La imagen muestra el resultado de ejecutar dicho código; en Firefox a la izquierda y en Chrome a la derecha. Observa cómo el catch no es invocado nunca (Firefox, a diferencia de Chrome, se queja de que una promise rechazada no ha sido tratada).

Como ya sabemos, para capturar errores en promises debemos usar el catch de la propia promise. Eso implica que si mezclamos llamadas a funciones asíncronas (que devuelvan promises) y funciones síncronas, el tratamiento de errores lo tenemos disperso: el catch del bloque try/catch nos captura los errores síncronos, y la función catch de la promise los errores en la promise rechazada.

Recuerda además, que dentro de una promise lanzar un error, rechaza la promise, por lo que en el siguiente código el bloque catch tampoco se ejecuta:

function asyncPromise() {
    return new Promise((resolve, reject) => {
        throw ("Unspecified error")
    });
}
 
try {
    asyncPromise().then(() => {
        console.log('asyncPromise run!');
    });
}
catch (error) {
    console.error('asyncPromise failed with: ' + error);
}

Ahora, a pesar de usar throw en asyncPromise el resultado es exactamente el mismo que antes: la función asyncPromise termina devolviendo una promise rechazada y el bloque catch no se llega a invocar.

Bloques try/catch y async/await

El uso de async/await soluciona esos problemas:

function asyncPromise() {
    return new Promise((resolve, reject) => {
        throw ("Unspecified error")
    });
}
 
async function test() {
    try {
        await asyncPromise();
        console.log('asyncPromise run!');
    }
    catch (error) {
        console.error('asyncPromise failed with: ' + error);
    }
}
 
test();

Observa que no se ha modificado asyncPromise() que sigue siendo una función tradicional que devuelve una promise. Pero ahora, se ha creado test() que es una función asíncrona (usa async) y realiza una espera asíncrona (await) sobre asyncPromise(). Pues bien, si asyncPromise() devuelve una promise rechazada, la espera asíncrona finaliza con error y el bloque catch se ejecuta.

Por lo tanto, al usar async/await es posible el uso de bloques try/catch para centralizar toda la gestión de errores, sean estos producidos por llamadas síncronas o asíncronas. Eso permite que el código sea mucho más claro.

Esperas asíncronas en paralelo

El siguiente código muestra una función que devuelve una promise que se resuelve al cabo de sec segundos:

function timeout(sec) {
      return new Promise(resolve => {
          setTimeout(() => { resolve(x);}, sec * 1000);
  });
}
 
¿Cuánto tiempo tarda en ejecutarse el siguiente código?
 
async function test() {
    await timeout(2);
    await timeout(3);
    console.log('¡Fin!');
}

Piénsalo un poco antes de responder: el código realiza dos esperas asíncronas por la función timeout. Una espera es de 2 segundos y la otra es de 3. ¿Cuánto tiempo estamos esperando?

La respuesta correcta es cinco. El primer await hace que nuestra función test se espere asíncronamente a la resolución de la promise que devuelve timeout(2), lo que no ocurre hasta que transcurren 2 segundos. Solo entonces, cuando la primera espera asíncrona ha sido finalizada, la ejecución de test continúa y lo hace con otra espera asíncrona de 3 segundos. Por lo que al cabo de unos 5 segundos, aparece el mensaje ¡Fin! en la consola.

Es decir, varios awaits sucesivos se ejecutan uno detrás de otro, no en paralelo. Pero… ¿existe alguna manera de realizar esperas asíncronas en paralelo?

Pues la respuesta es que sí, y pasa por tener presente que await espera por una promise, así que nos basta con crear una promise que ejecute en paralelo dos o mas promises y que se resuelva tan pronto se resuelvan las otras a las que envuelve. Y si recuerdas el módulo de promises, cuando mencionamos la composición de promises hablamos sobre Promise.all:

async function test() {
    await Promise.all([timeout(2), timeout(3)]);
    console.log('¡Fin!');
}

Ahora el await espera por la promesa devuelta por Promise.all que ejecuta en paralelo las dos promises y se resuelve cuando ambas son resueltas, lo que ocurre aproximadamente al cabo de, esta vez sí, tres segundos.

Ejercicio propuesto

informatica/programacion/cursos/programacion_avanzada_javascript/funciones_asincronas.1729069147.txt.gz · Última modificación: por tempwin