preloader

Async/Await

Hay una sintaxis especial para trabajar con promesas de una forma más cómoda, llamada «async/await». Es sorprendentemente sencilla de entender y usar.

Funciones async

Empecemos con la palabra clave async. Puede ubicarse delante de una función como en el siguiente ejemplo:

				
					async function f() {
  return 1;
}

				
			

La palabra async delante de una función significa una sola cosa: la función siempre devolverá una promesa. Cualquier otro valor será envuelto en una promesa automáticamente.

Por ejemplo, esta función devuelve una promesa que se resuelve con el valor de 1. Probémoslo:

				
					async function f() {
  return 1;
}

f().then(alert); // 1

				
			

La palabra async delante de una función significa una sola cosa: la función siempre devolverá una promesa. Cualquier otro valor será envuelto en una promesa automáticamente.

Por ejemplo, esta función devuelve una promesa que se resuelve con el valor de 1. Probémoslo:

				
					async function f() {
  return 1;
}

f().then(alert); // 1

				
			

Podríamos devolver explícitamente una promesa, lo cual sería lo mismo:

				
					async function f() {
  return Promise.resolve(1);
}

f().then(alert); // 1

				
			

Entonces, async se asegura de que la función devuelva una promesa, o envuelve los valores no promesas y los convierte en una. Bastante simple, ¿verdad? Pero hay otra palabra clave, await, que solo funciona dentro de funciones async y es muy interesante.

Await

La sintaxis:

				
					// funciona solo dentro de funciones async
let value = await promise;

				
			

await hace que JavaScript espere hasta que la promesa se resuelva y devuelve su resultado.

Aquí hay un ejemplo con una promesa que se resuelve en 1 segundo:

				
					async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("¡Listo!"), 1000);
  });

  let result = await promise; // espera hasta que la promesa se resuelva (*)

  alert(result); // "¡Listo!"
}

f();

				
			

La ejecución de la función se detiene en la línea (*) y se reanuda cuando la promesa se resuelve, con result obteniendo su resultado. Entonces, el código anterior muestra «¡Listo!» después de un segundo.

Enfatizamos: await literalmente pausa la ejecución de la función hasta que la promesa se resuelva, y luego la reanuda con el resultado de la promesa. Esto no consume recursos de CPU, ya que el motor de JavaScript puede realizar otras tareas mientras tanto: ejecutar otros scripts, manejar eventos, etc.

Es simplemente una sintaxis más elegante para obtener el resultado de una promesa en comparación con promise.then, es más fácil de leer y escribir.

No se puede usar await en funciones comunes

Si intentamos usar await en una función que no es async, obtendremos un error de sintaxis:

				
					function f() {
  let promise = Promise.resolve(1);
  let result = await promise; // Syntax error
}

				
			

Es posible que obtengamos este error si olvidamos poner async delante de una función. Como se mencionó, await solo funciona dentro de una función async.

Tomemos el ejemplo showAvatar() del capítulo sobre encadenamiento de promesas y reescribámoslo usando async/await:

Necesitaremos reemplazar las llamadas .then con await. También debemos hacer que la función sea async para que funcionen.

				
					async function showAvatar() {

  // leer nuestro JSON
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();

  // leer usuario de GitHub
  let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
  let githubUser = await githubResponse.json();

  // muestra el avatar
  let img = document.createElement('img');
  img.src = githubUser.avatar_url;
  img.className = "promise-avatar-example";
  document.body.append(img);

  // espera 3 segundos
  await new Promise((resolve, reject) => setTimeout(resolve, 3000));

  img.remove();

  return githubUser;
}

showAvatar();

				
			

Bastante limpio y fácil de leer, ¿verdad? Mucho mejor que antes.

Los navegadores modernos permiten await en el nivel superior de los módulos

En los navegadores modernos, await en el nivel superior funciona, siempre que estemos dentro de un módulo. Cubriremos módulos en el artículo sobre módulos.

Por ejemplo:

				
					// asumimos que este código se ejecuta en el nivel superior dentro de un módulo
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();

console.log(user);

				
			

Si no estamos usando módulos, o necesitamos soportar navegadores antiguos, hay una receta universal: envolver el código en una función async anónima.

Así:

				
					(async () => {
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
  // ...
})();

				
			

Await acepta “thenables”

Al igual que promise.then, await permite el uso de objetos “thenable” (aquellos con el método then). La idea es que un objeto de terceros puede no ser una promesa, pero ser compatible con una: si soporta .then, es suficiente para el uso con await.

Aquí hay una demostración de la clase Thenable; el await a continuación acepta sus instancias:

				
					class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve);
    // resuelve con this.num * 2 después de 1000ms
    setTimeout(() => resolve(this.num * 2), 1000); // (*)
  }
}

async function f() {
  // espera durante 1 segundo, entonces el resultado se vuelve 2
  let result = await new Thenable(1);
  alert(result);
}

f();

				
			

Si await obtiene un objeto no-promesa con .then, llama a dicho método proporcionándole las funciones incorporadas resolve y reject como argumentos (exactamente como lo hace con los ejecutores de promesas regulares). Entonces await espera hasta que uno de ellos sea llamado (en el ejemplo anterior esto ocurre en la línea (*)) y luego procede con el resultado.

Métodos de clase async

Para declarar un método de clase async, simplemente se le antepone async:

				
					class Waiter {
  async wait() {
    return await Promise.resolve(1);
  }
}

new Waiter()
  .wait()
  .then(alert); // 1 (lo mismo que (result => alert(result)))

				
			

El significado es el mismo: asegura que el valor devuelto es una promesa y habilita await.

Manejo de errores

Si una promesa se resuelve normalmente, entonces await promise devuelve el resultado. Pero en caso de rechazo, lanza un error, tal como si hubiera una instrucción throw en esa línea.

Este código:

				
					async function f() {
  await Promise.reject(new Error("¡Ups!"));
}

				
			

…es lo mismo que esto:

				
					async function f() {
  throw new Error("¡Ups!");
}

				
			

En situaciones reales, la promesa tomará algún tiempo antes del rechazo. En tal caso, habrá un retardo antes de que await lance un error.

Podemos atrapar dicho error usando try..catch, de la misma manera que con un throw normal:

				
					async function f() {

  try {
    let response = await fetch('http://no-such-url');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

				
			

En caso de un error, el control salta al bloque catch. Podemos también envolver múltiples líneas:

				
					async function f() {

  try {
    let response = await fetch('/no-user-here');
    let user = await response.json();
  } catch(err) {
    // atrapa errores tanto en fetch como en response.json
    alert(err);
  }
}

f();

				
			

Si no tenemos try..catch, entonces la promesa generada por la llamada a la función async f() se vuelve rechazada. Podemos añadir .catch para manejarlo:

				
					async function f() {
  let response = await fetch('http://no-such-url');
}

// f() se vuelve una promesa rechazada
f().catch(alert); // TypeError: failed to fetch // (*)

				
			

Si olvidamos añadir .catch allí, obtendremos un error de promesa no gestionado (visible en la consola). Podemos atrapar tales errores usando un manejador de evento global unhandledrejection, como se describe en el capítulo sobre manejo de errores con promesas.

async/await y promise.then/catch

Cuando usamos async/await, rara vez necesitamos .then, porque await maneja la espera por nosotros. Y podemos usar un try..catch normal en lugar de .catch. Esto usualmente (no siempre) es más conveniente.

Pero en el nivel superior del código, cuando estamos fuera de cualquier función async, no estamos sintácticamente habilitados para usar await, entonces es una práctica común agregar .then/catch para manejar el resultado final o los errores que se produzcan, como en la línea (*) del ejemplo anterior.

async/await funciona bien con Promise.all

Cuando necesitamos esperar múltiples promesas, podemos envolverlas en un Promise.all y luego usar await:

				
					// espera por el array de resultados
let results = await Promise.all([
  fetch(url1),
  fetch(url2),
  // ...
]);

				
			

En caso de error, se propaga como es usual, desde la promesa que falla a Promise.all, y luego se convierte en una excepción que podemos atrapar usando try..catch alrededor de la llamada.

Resumen

El comando async antes de una función tiene dos efectos:

  1. Hace que siempre devuelva una promesa.
  2. Permite que se use await dentro de ella.

El comando await antes de una promesa hace que JavaScript espere hasta que la promesa se resuelva. Entonces:

  • Si es un error, se lanza una excepción, lo mismo que si se llamara a throw error en ese mismo lugar.
  • De lo contrario, devuelve el resultado.

Juntos proveen un excelente marco para escribir código asincrónico que es fácil de leer y escribir.

Con async/await raramente necesitamos escribir promise.then/catch, pero aún no debemos olvidar que están basados en promesas porque a veces (por ejemplo, en el nivel superior del código) tenemos que usar esos métodos. También Promise.all es adecuado cuando esperamos varias tareas simultáneas.

Related Post

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *