Funciones asíncronas con async/await

Javascript

En este tutorial explicaremos cómo crear funciones asíncronas usando la sintaxis async y await, que son una evolución de las promesas de JavaScript.

Introducción

El código asíncrono es aquel que sale del hilo principal de ejecución de JavaScript, ejecutándose en paralelo. Si bien JavaScript es un lenguaje síncrono por naturaleza, existen ciertas funcionalidades en los entornos de ejecución de JavaScript o en los navegadores que permiten la ejecución de código asíncrono.

Inicialmente, el código asíncrono en JavaScript se ejecutaba únicamente gracias al uso de callbacks. Sin embargo, en la versión ES2015 de JavaScript se introdujeron las promesas para evitar ciertos problemas que hacían que el uso de callbacks se complicase bastante cuando se anidaban. Pero con el objetivo de facilitar todavía más su uso, se introdujo la sintaxis async/await en la versión ES2017 de JavaScript.

Esta sintaxis permite crear funciones que usan tanto promesas como generadores, siendo una abstracción de alto nivel de las promesas de JavaScript. La única diferencia es que las funciones que usan async/await, gracias al uso de generadores, pueden pausarse a sí mismas y continuar su ejecución en otro momento.

Qué es al sintaxis async/away

Cuando las promesas aparecieron en la versión ES2015 de JavaScript, resolvían los problemas que se daban con el uso de callbacks a la hora de gestionar el código asíncrono. El mayor era el callback hell, que se daba cuando se anidaban varios callbacks que ejecutaban código asíncrono, ya que el código se volvía complicado y poco legible.

Sin embargo, las promesas eran mejorables, ya que también agregaban su propia complejidad y una sintaxis difícil de entender. Por ello, aparecieron las funciones asíncronas, que usan la sintaxis async/await, permitiendo seguir un estilo de programación síncrono aún creando funciones asíncronas.

Cómo crear una función asíncrona con async

Para crear una función asíncrona con async, bastará con que antepongas el término async antes de la definición de la función:

async function obtenerDatos() {
  return 'hola!';
}

Esa función no gestiona código asíncrono, pero funciona de un modo diferente a las funciones tradicionales, ya que cuando la ejecutes podrás comprobar que devuelve una promesa. De hecho, si inspeccionas el código, podrás comprobar que la promesa que es devuelta incluye las propiedades PromiseStatus y PromiseValue. Para comprobarlo, ejecuta la función obtenerDatos:

console.log(obtenerDatos());

Podrás ver este resultado por la consola:

__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: Object

Esto significa que podrás gestionar cualquier función que use async como una promesa, pudiendo utilizar then, catch o finally al igual que lo harías en una promesa:

obtenerDatos().then(response => console.log(response));

Al ejecutar el código anterior deberías ver el siguiente resultado por la consola:

hola!

Cuando usas async con una función, se devolverá una promesa aunque no la devuelvas explícitamente, ya que el entorno de ejecución de JavaScript se encargará de que así sea. Por ello, las dos funciones que ves a continuación son equivalentes:

const funcionA = async () => {
  return 'Hola!';
}

funcionA().then((resultado) => console.log(resultado));

const functionB = async () => {
  return Promise.resolve('Hola!');
}

funcionB().then((resultado) => console.log(resultado));

Cómo usar await en una función asíncrona

Las funciones asíncronas permiten ejecutar promesas en su interior mediante el operador await, que esperará a que la promesa revuelva un valor antes de continuar la ejecución del código.

Vamos a poner como ejemplo la siguiente promesa:

const tareaAsincrona = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve('Tarea completada'), 3000);
  })
}

Mediante el uso de await en una función asíncrona, podremos ejecutar la promesa y obtener su resultado, pausando la ejecución de la función hasta que la promesa devuelva un resultado.

async function gestionarTareas() {
  const response = await tareaAsincrona();
  console.log(response);
}

Tal y como has visto, puedes usar await en las funciones asíncronas declaradas con async. Del mismo modo, también puedes usar await con la función fetch para realizar una petición a una API.

En el siguiente ejemplo obtenemos los datos de un usuario desde GitHub usando la API Fetch en una función asíncrona:

async function obtenerUsuario() {
  const response = await fetch('https://api.github.com/users/edulazaro');
  const data = await response.json();

  console.log(data);
}

obtenerUsuario();

El operador await que anteponemos a la función fetch en el interior de la función obtenerUsuario evitará que la línea siguiente se ejecute hasta que no obtengamos un valor de vuelta. De este modo nos aseguramos de que la constante data no esté vacía. Como ves, no es necesario usar then, como sí hacemos con las promesas, para obtener el resultado.

Este será el resultado que se muestre por la consola:

blog: "https://www.neoguias.com"
twitter_username: "neeonez"
type: "User"
url: "https://api.github.com/users/edulazaro"
updated_at: "2021-01-14T11:22:45Z"
....

Información! En muchos entornos solamente es posible usar await en el interior de una función definida con async. Sin embargo, es posible usar await fuera de estas funciones en las últimas versiones de los navegadores más utilizados y del Node.js, evitando la necesidad de crear una función con async.
Sin embargo, existe una gran diferencia entre el uso de await y el uso de then() con una promesa. El uso de await suspende la ejecución de la función actual hasta que el código de contexto superior se termine de ejecutar, mientras que usando then(), el cierpo de la promsa se seguirá ejecutando. Vamos a verlo con un ejemplo:

const miFuncion= () => Promise.resolve('Tres');

async function  miFuncion() {
  console.log('Dos');
  const res = await getC();
  console.log(res);
}

console.log('Uno')
miFuncion();
console.log('Cuatro');

En el ejemplo anterior, primero se mostrará Uno por la consola, luego Dos, seguidamente Cuatro y finalmente Tres. Esto es debido a que, cuando se ejecuta la función miFuncion(), se obtiene el resultado de getC(), pero antes de ser asignado a la constante sres, dado que hemos usado await, se finalizará primero todo el código de los contextos superiores. Por ello, antes de que se muestre Tres por pantalla, se ejecutará primero el console.log(‘Cuatro’) y el resto del código de dicho contexto, de haberlo. Al finalizar, se retoma la ejecución de la función miFuncion(), asignándose Tres a la constante res y ejecutándose finalmente el console.log(res).

Si hubiésemos usando un await con la función miFuncion(), de estar la llamada en el interior de una función asíncrona, el resultado sería otro. Primero se mostraría Uno por la consola, luego Dos, seguidamente Tres y finalmente Cuatro

Cómo gestionar errores con catch

Dado que estamos trabajando con funciones asíncronas, también podrás gestionar los errores de la misma. Cuando usas una promesa, tienes disponible el método catch, usado junto al método then, que te permite capturar los posible errores que ocurran durante su ejecución. Sin embargo, en este caso usaremos una sentencia try/catch para obtener el mismo efecto.

Vamos a gestionar las posibles excepciones del ejemplo del apartado anterior, en el que obteníamos un usuario de GitHub usando fetch:

async function obtenerUsuario() {
  try{
    // Si todo ha ido bien
    const response = await fetch('https://api.github.com/users/edulazaro');
    const data = await response.json(); console.log(data);
  } catch(error) {
    // Ha ocurrido algún error
    console.log(error);
  }
}

obtenerUsuario();

El código anterior saltará hasta la sentencia catch en caso de que reciba un error, mostrándolo por la consola.

Por qué debes usar async/await

Tal y como has visto, el código es mucho más fácil de entender cuando usas async/await en comparación con el uso de promesas. Sin  embargo, este fenómeno se acentúa más cuando en una promesa usarías varias sentencias then en cadena, siendo cuando más percibas sus ventajas.

Vamos a ver un ejemplo algo más complejo en el que obtendremos un archivo JSON para luego realizar varias tareas con él. Primero usaremos una promesa y luego async/await.

En el código que ves a continuación obtenemos un archivo JSON. Luego obtenemos el primer elemento del mismo y seguidamente realizamos una petición a una API para obtener un recurso, para finalmente transformar el resultado a formato JSON:

const obtenerPrimeraFila = () => {
  return fetch('/filas.json')
    .then(res => res.json())
    .then(filas => filas[0])
    .then(fila => fetch(`/filas/${fila.id}`))
    .then(res => res.json());
}

obtenerPrimeraFila();

Si ahora usamos async/await, comprobarás que el código es más sencillo:

const obtenerPrimeraFila = async () => {
  const res = await fetch('/filas.json');
  const filas = await res.json();
  const fila = filas[0];
  const resFila = await fetch(`/filas/${fila.id}`);
  const datos = await resFila.json();
  return datos;
}

obtenerPrimeraFila();

Otra ventaja del usar  async/await es que la depuración del código será muy sencilla en comparación con el uso de promesas. Esto es debido a que por defecto, el depurador no parará su ejecución con el código asíncrono, pero sí lo hará cuando uses async/await, ya que se ejecuta como el código síncrono.

Cómo encadenar funciones asíncronas

Las funciones asíncronas también pueden encadenarse al igual que las promesas, y además con una sintaxis más fácil de entender, ya que bastará con usar el operador +. A continuación puedes ver un ejemplo:

const comerFruta = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve('Me gusta comer fruta'), 1000);
  });
}

const comerFrutaVerdura = async () => {
  const fruta = await comerFruta();
  return fruta + ' y verdura';
}

comerFrutaVerdura().then(res => {
  console.log(res);
});

El código anterior mostrará el siguiente mensaje por la consola cuando ejecutes la función comerFruta():

comerFruta(); // Me gusta comer fruta y verdura

Limitaciones del uso de async/await

Actualmente resulta mucho más frecuente el uso de async/await que el uso de promesas. Sin embargo, las promesas disponen de funcionalidades adicionales que no podrás conseguir con async/await. Un ejemplo de ello es la combinación de varias promesas mediante Promise.all() o el método Promise.race().

En el fondo, cuando usas async/await estás usando promesas junto con generadores, que son capaces de pausar la ejecución del código, haciendo que el código sea más flexible.

Alternativas a async/await

En la actualidad, lo recomendable es que uses async/await siempre que sea posible en lugar de usar promesas directamente, ya que el código será más legible y fácil de entender por otros desarrolladores.

Sin embargo, en ocasiones puede que sea necesario usar callbacks o promesas debido a las limitaciones de async/await. Si quieres obtener más información acerca de su uso, consulta los siguientes tutoriales:

Y esto ha sido todo.


Avatar de Edu Lazaro

Edu Lázaro: Ingeniero técnico en informática, actualmente trabajo como desarrollador web y programador de videojuegos.

👋 Hola! Soy Edu, me encanta crear cosas y he redactado esta guía. Si te ha resultado útil, el mayor favor que me podrías hacer es el de compatirla en Twitter 😊

Si quieres conocer mis proyectos, sígueme en Twitter.

2 comentarios en “Funciones asíncronas con async/await

  1. en el ejemplo de
    [
    const miFuncion= () => Promise.resolve(‘Tres’);

    async function miFuncion() {
    console.log(‘Dos’);
    const res = await getC();
    console.log(res);
    }

    console.log(‘Uno’)
    miFuncion();
    console.log(‘Cuatro’);
    ]

    declaras dos veces la funcion ‘miFuncion’ , no se si lo has escrito mal o yo no lo comprendo, no se puede ejecutar ese codigo

Deja una respuesta

“- Hey, Doc. No tenemos suficiente carretera para ir a 140/h km. - ¿Carretera? A donde vamos, no necesitaremos carreteras.”