====== Funciones asíncronas ======
Módulo perteneciente al curso [[informatica:programacion:cursos:programacion_avanzada_javascript|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()''.
{{ :informatica:programacion:cursos:programacion_avanzada_javascript:12-invocacion-funcion-asincrona.png |}}
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'':
{{ :informatica:programacion:cursos:programacion_avanzada_javascript:12-invocacion-funcion-asincrona-then.png |}}
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''), 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 //[[https://en.wikipedia.org/wiki/Syntactic_sugar|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 "[[https://es.wikipedia.org/wiki/Ping|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 [[https://developer.mozilla.org/en-US/docs/Web/API/Performance/now|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:
{{ :informatica:programacion:cursos:programacion_avanzada_javascript:12-error-promise-no-capturado.png |}}
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 =====
Crea una función que, usando la API ''fetch'', devuelva una //promise// resuelta con el número de carácteres de la respuesta de una URI. La función debe invocarse como sigue:
uriSize('https://jsonplaceholder.typicode.com/posts/42')
    .then(s => console.log('Num carácteres recibidos:' + s))
Crea la función asíncrona ''getLargestUri'' que espere un array de cadenas con varias direcciones URI y que devuelva la que ofrezca la respuesta más larga en bytes. Para sus cálculos haz que use la función ''uriSize'' anterior. Debe invocarse como sigue:
getLargestUri([
    'https://jsonplaceholder.typicode.com/posts/4',
    'https://jsonplaceholder.typicode.com/posts/5',
    'https://jsonplaceholder.typicode.com/posts/42'])
    .then(v => console.log('La url más larga es ' + v.url + ' con ' + v.size + ' caracteres'));
Intenta que la función ''getLargestUri'' haga las peticiones en paralelo:
{{ :informatica:programacion:cursos:programacion_avanzada_javascript:12-ejercicio-peticiones-paralelo.png |}}
==== Consejos ====
En contra de lo que pueda parecer, este es un **ejercicio relativamente complejo**, así que tómatelo con calma y repasa lo necesario del módulo de fetch API.
La función ''getLargestUri'' es la que presenta el mayor reto. Repasa la sección de "Esperas asíncronas en paralelo" para ver lo que debes hacer para llamar asíncronamente N veces la función ''uriSize'' pero que se ejecuten todas en paralelo.
==== Y un pequeño reto adicional ====
En la función ''getLargestUri'' al final, de los N resultados devueltos por las N llamadas a ''uriSize'' debes seleccionar aquella cuyo valor de ''size'' sea mayor. Seguramente hayas implementado (o tengas pensado implementar) eso mediante un bucle. No hay problema, pero ya puestos te propongo un pequeño reto adicional:
Crea una función ''max'' que se pueda llamar sobre **cualquier array**. Dicha función debe esperar una función transformadora (que transforma el elemento del array en un número) y debe devolver el elemento cuyo valor transformado en número es mayor. La función debe invocarse de este modo:
[{v:42, name:'great question'}, {v: 0, name: 'zero'}].max(i => i.v)
(En este caso debe devolver el primer elemento)
Una vez tengas esa función ''max'', úsala en ''getLargestUri'' para seleccionar el elemento con el tamaño mayor.