Comprender el bucle de eventos, devoluciones de llamada, promesas y async/await en JavaScript

El autor seleccionó el Fondo de Ayuda COVID-19 para recibir una donación como parte del programa Escribir para Donaciones.

Introducción

En los primeros días de internet, los sitios web a menudo consistían en datos estáticos en una página HTML. Pero ahora que las aplicaciones web se han vuelto más interactivas y dinámicas, se ha vuelto cada vez más necesario realizar operaciones intensivas como hacer solicitudes de red externas para recuperar datos de API. Para manejar estas operaciones en JavaScript, un desarrollador debe usar técnicas de programación asíncrona.

Dado que JavaScript es un lenguaje de programación de un solo hilo con un modelo de ejecución síncrono que procesa una operación tras otra, solo puede procesar una declaración a la vez. Sin embargo, una acción como solicitar datos de una API puede llevar un tiempo indeterminado, dependiendo del tamaño de los datos solicitados, la velocidad de la conexión de red y otros factores. Si las llamadas a la API se realizaran de manera síncrona, el navegador no podría manejar ninguna entrada del usuario, como desplazarse o hacer clic en un botón, hasta que se complete esa operación. Esto se conoce como bloqueo.

Para evitar comportamientos de bloqueo, el entorno del navegador tiene muchas APIs web a las que JavaScript puede acceder que son asincrónicas, lo que significa que pueden ejecutarse en paralelo con otras operaciones en lugar de secuencialmente. Esto es útil porque permite al usuario seguir utilizando el navegador normalmente mientras se procesan las operaciones asincrónicas.

Como desarrollador de JavaScript, necesitas saber cómo trabajar con APIs web asincrónicas y manejar la respuesta o error de esas operaciones. En este artículo, aprenderás sobre el bucle de eventos, la forma original de lidiar con el comportamiento asincrónico a través de callbacks, la actualización de ECMAScript 2015 con la adición de promesas y la práctica moderna de usar async/await.

Nota: Este artículo se enfoca en JavaScript del lado del cliente en el entorno del navegador. Los mismos conceptos son generalmente ciertos en el entorno de Node.js, sin embargo, Node.js utiliza sus propias APIs de C++ en lugar de las APIs web del navegador. Para más información sobre la programación asincrónica en Node.js, consulta Cómo escribir código asincrónico en Node.js.

El bucle de eventos

Esta sección explicará cómo JavaScript maneja el código asíncrono con el bucle de eventos. Primero se ejecutará una demostración del bucle de eventos en acción, y luego se explicarán los dos elementos del bucle de eventos: la pila y la cola.

El código JavaScript que no utiliza ninguna API web asíncrona se ejecutará de manera sincrónica, uno a la vez, secuencialmente. Esto se demuestra con este código de ejemplo que llama a tres funciones que imprimen un número en la consola:

// Define tres funciones de ejemplo
function first() {
  console.log(1)
}

function second() {
  console.log(2)
}

function third() {
  console.log(3)
}

En este código, se definen tres funciones que imprimen números con console.log().

A continuación, se escriben las llamadas a las funciones:

// Ejecutar las funciones
first()
second()
third()

La salida se basará en el orden en que se llamaron las funciones: first(), second(), luego third():

Output
1 2 3

Cuando se utiliza una API web asíncrona, las reglas se vuelven más complicadas. Una API integrada con la que puedes probar esto es setTimeout, que establece un temporizador y realiza una acción después de un tiempo especificado. setTimeout necesita ser asíncrono, de lo contrario, todo el navegador permanecería congelado durante la espera, lo que resultaría en una mala experiencia para el usuario.

Agrega setTimeout a la función second para simular una solicitud asíncrona:

// Define tres funciones de ejemplo, pero una de ellas contiene código asíncrono
function first() {
  console.log(1)
}

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

function third() {
  console.log(3)
}

setTimeout toma dos argumentos: la función que se ejecutará de forma asíncrona y la cantidad de tiempo que esperará antes de llamar a esa función. En este código envolviste console.log en una función anónima y la pasaste a setTimeout, luego estableciste que la función se ejecute después de 0 milisegundos.

Ahora llama a las funciones, como lo hiciste antes:

// Ejecutar las funciones
first()
second()
third()

Podrías esperar que con un setTimeout establecido en 0 milisegundos, al ejecutar estas tres funciones todavía se imprimirían los números en orden secuencial. Pero debido a que es asíncrono, la función con el temporizador se imprimirá al final:

Output
1 3 2

Ya sea que establezcas el tiempo de espera en cero segundos o cinco minutos, no hará ninguna diferencia: el console.log llamado por código asíncrono se ejecutará después de las funciones síncronas de nivel superior. Esto sucede porque el entorno hospedador de JavaScript, en este caso el navegador, utiliza un concepto llamado el bucle de eventos para manejar la concurrencia o eventos paralelos. Dado que JavaScript solo puede ejecutar una instrucción a la vez, necesita que el bucle de eventos sea informado de cuándo ejecutar qué instrucción específica. El bucle de eventos maneja esto con los conceptos de una pila y una cola.

Pila

El stack, o pila de llamadas, mantiene el estado de qué función se está ejecutando actualmente. Si no estás familiarizado con el concepto de una pila, puedes imaginarla como un array con propiedades de “Último en entrar, primero en salir” (LIFO), lo que significa que solo puedes añadir o eliminar elementos del final de la pila. JavaScript ejecutará el marco actual (o llamada de función en un entorno específico) en la pila, luego lo eliminará y pasará al siguiente.

Para el ejemplo que contiene solo código síncrono, el navegador maneja la ejecución en el siguiente orden:

  • Agregar first() a la pila, ejecutar first() que registra 1 en la consola, eliminar first() de la pila.
  • Agregar second() a la pila, ejecutar second() que registra 2 en la consola, eliminar second() de la pila.
  • Agregar third() a la pila, ejecutar third() que registra 3 en la consola, eliminar third() de la pila.

El segundo ejemplo con setTimeout se ve así:

  • Agregar first() a la pila, ejecutar first() que registra 1 en la consola, eliminar first() de la pila.
  • Agregar second() a la pila, ejecutar second().
    • Agregar setTimeout() a la pila, ejecutar la API web setTimeout() que inicia un temporizador y añade la función anónima a la cola, eliminar setTimeout() de la pila.
  • Elimine second() de la pila.
  • Agregue third() a la pila, ejecute third() que registra 3 en la consola, elimine third() de la pila.
  • El bucle de eventos verifica la cola en busca de mensajes pendientes y encuentra la función anónima de setTimeout(), agrega la función a la pila que registra 2 en la consola, luego la elimina de la pila.

Usar setTimeout, una API web asíncrona, introduce el concepto de la cola, que este tutorial cubrirá a continuación.

Cola

La cola, también conocida como cola de mensajes o cola de tareas, es un área de espera para funciones. Cuando la pila de llamadas está vacía, el bucle de eventos verificará la cola en busca de mensajes en espera, comenzando desde el mensaje más antiguo. Una vez que encuentra uno, lo agregará a la pila, que ejecutará la función en el mensaje.

En el ejemplo de setTimeout, la función anónima se ejecuta inmediatamente después del resto de la ejecución de nivel superior, ya que el temporizador se estableció en 0 segundos. Es importante recordar que el temporizador no significa que el código se ejecutará exactamente en 0 segundos o en el tiempo especificado, sino que agregará la función anónima a la cola en esa cantidad de tiempo. Este sistema de cola existe porque si el temporizador agregara la función anónima directamente a la pila cuando el temporizador finaliza, interrumpiría cualquier función que se esté ejecutando actualmente, lo que podría tener efectos no deseados e impredecibles.

Nota: También hay otra cola llamada la cola de trabajos o cola de microtareas que maneja las promesas. Las microtareas como las promesas se manejan con una prioridad más alta que las macrotareas como setTimeout.

Ahora sabes cómo el bucle de eventos utiliza la pila y la cola para manejar el orden de ejecución del código. La próxima tarea es descubrir cómo controlar el orden de ejecución en tu código. Para hacer esto, primero aprenderás sobre la forma original de asegurar que el código asíncrono se maneje correctamente por el bucle de eventos: las funciones de devolución de llamada.

Funciones de Devolución de Llamada

En el ejemplo de setTimeout, la función con el temporizador se ejecutó después de todo en el contexto de ejecución principal de nivel superior. Pero si quisieras asegurarte de que una de las funciones, como la función third, se ejecutara después del temporizador, entonces tendrías que usar métodos de codificación asíncrona. El temporizador aquí puede representar una llamada de API asíncrona que contiene datos. Quieres trabajar con los datos de la llamada de API, pero tienes que asegurarte de que los datos se devuelvan primero.

La solución original para lidiar con este problema es usar funciones de devolución de llamada. Las funciones de devolución de llamada no tienen una sintaxis especial; solo son una función que se ha pasado como argumento a otra función. La función que toma otra función como argumento se llama una función de orden superior. Según esta definición, cualquier función puede convertirse en una función de devolución de llamada si se pasa como argumento. Las devoluciones de llamada no son asíncronas por naturaleza, pero se pueden usar con fines asíncronos.

Aquí tienes un ejemplo de código sintáctico de una función de orden superior y una devolución de llamada:

// Una función
function fn() {
  console.log('Just a function')
}

// Una función que toma otra función como argumento
function higherOrderFunction(callback) {
  // Cuando llamas a una función que se pasa como argumento, se denomina devolución de llamada
  callback()
}

// Pasando una función
higherOrderFunction(fn)

En este código, defines una función fn, defines una función higherOrderFunction que toma una función callback como argumento y pasas fn como una devolución de llamada a higherOrderFunction.

Ejecutar este código dará lo siguiente:

Output
Just a function

Volviendo al primer, segundo y tercer funciones con setTimeout. Esto es lo que tienes hasta ahora:

function first() {
  console.log(1)
}

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

function third() {
  console.log(3)
}

La tarea es hacer que la ejecución de la tercer función siempre se retrase hasta que se complete la acción asíncrona en la segundo función. Aquí es donde entran los callbacks. En lugar de ejecutar primer, segundo y tercer a nivel superior de ejecución, pasarás la función tercer como argumento a segundo. La función segundo ejecutará el callback después de que la acción asíncrona haya terminado.

Aquí están las tres funciones con un callback aplicado:

// Define tres funciones
function first() {
  console.log(1)
}

function second(callback) {
  setTimeout(() => {
    console.log(2)

    // Ejecutar la función de callback
    callback()
  }, 0)
}

function third() {
  console.log(3)
}

Ahora, ejecuta primer y segundo, luego pasa tercer como argumento a segundo:

first()
second(third)

Después de ejecutar este bloque de código, recibirás la siguiente salida:

Output
1 2 3

Primero se imprimirá 1, y después de que el temporizador se complete (en este caso, cero segundos, pero puedes cambiarlo a cualquier cantidad) se imprimirá 2 luego 3. Al pasar una función como callback, has retrasado con éxito la ejecución de la función hasta que se complete la API web asíncrona (setTimeout).

La lección clave aquí es que las funciones de devolución de llamada no son asíncronas—setTimeout es la API web asíncrona responsable de manejar tareas asíncronas. La devolución de llamada simplemente te permite ser informado de cuándo ha completado una tarea asíncrona y maneja el éxito o el fracaso de la tarea.

Ahora que has aprendido cómo usar devoluciones de llamada para manejar tareas asíncronas, la siguiente sección explica los problemas de anidar demasiadas devoluciones de llamada y crear una “pirámide del caos.”

Devoluciones de Llamada Anidadas y la Pirámide del Caos

Las funciones de devolución de llamada son una forma efectiva de asegurar la ejecución retrasada de una función hasta que otra se complete y devuelva datos. Sin embargo, debido a la naturaleza anidada de las devoluciones de llamada, el código puede terminar siendo desordenado si tienes muchas solicitudes asíncronas consecutivas que dependen entre sí. Esto fue una gran frustración para los desarrolladores de JavaScript al principio, y como resultado, el código que contiene devoluciones de llamada anidadas a menudo se llama “pirámide del caos” o “infierno de devolución de llamada.”

Aquí hay una demostración de devoluciones de llamada anidadas:

function pyramidOfDoom() {
  setTimeout(() => {
    console.log(1)
    setTimeout(() => {
      console.log(2)
      setTimeout(() => {
        console.log(3)
      }, 500)
    }, 2000)
  }, 1000)
}

En este código, cada nuevo setTimeout está anidado dentro de una función de orden superior, creando una forma de pirámide de devoluciones de llamada cada vez más profundas. Ejecutar este código daría como resultado lo siguiente:

Output
1 2 3

En la práctica, con código asíncrono del mundo real, esto puede volverse mucho más complicado. Lo más probable es que necesites manejar errores en el código asíncrono y luego pasar algunos datos de cada respuesta a la siguiente solicitud. Hacer esto con devoluciones de llamada hará que tu código sea difícil de leer y mantener.

Aquí tienes un ejemplo ejecutable de un “pirámide del infierno” más realista con el que puedes experimentar:

// Función asíncrona de ejemplo
function asynchronousRequest(args, callback) {
  // Lanzar un error si no se pasan argumentos
  if (!args) {
    return callback(new Error('Whoa! Something went wrong.'))
  } else {
    return setTimeout(
      // Simplemente añadiendo un número aleatorio para que parezca que la función asíncrona forzada
      // devolvió datos diferentes
      () => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
      500,
    )
  }
}

// Solicitudes asíncronas anidadas
function callbackHell() {
  asynchronousRequest('First', function first(error, response) {
    if (error) {
      console.log(error)
      return
    }
    console.log(response.body)
    asynchronousRequest('Second', function second(error, response) {
      if (error) {
        console.log(error)
        return
      }
      console.log(response.body)
      asynchronousRequest(null, function third(error, response) {
        if (error) {
          console.log(error)
          return
        }
        console.log(response.body)
      })
    })
  })
}

// Ejecutar
callbackHell()

En este código, debes hacer que cada función tenga en cuenta una posible respuesta y un posible error, lo que hace que la función callbackHell sea visualmente confusa.

Al ejecutar este código, obtendrás lo siguiente:

Output
First 9 Second 3 Error: Whoa! Something went wrong. at asynchronousRequest (<anonymous>:4:21) at second (<anonymous>:29:7) at <anonymous>:9:13

Esta forma de manejar código asíncrono es difícil de seguir. Como resultado, se introdujo el concepto de promesas en ES6. Esto es el foco de la siguiente sección.

Promesas

A promise represents the completion of an asynchronous function. It is an object that might return a value in the future. It accomplishes the same basic goal as a callback function, but with many additional features and a more readable syntax. As a JavaScript developer, you will likely spend more time consuming promises than creating them, as it is usually asynchronous Web APIs that return a promise for the developer to consume. This tutorial will show you how to do both.

Creando una Promesa

Puedes inicializar una promesa con la sintaxis new Promise, y debes inicializarla con una función. La función que se pasa a una promesa tiene los parámetros resolve y reject. Las funciones resolve y reject manejan el éxito y el fracaso de una operación, respectivamente.

Escribe la siguiente línea para declarar una promesa:

// Inicializar una promesa
const promise = new Promise((resolve, reject) => {})

Si inspeccionas la promesa inicializada en este estado con la consola del navegador web, encontrarás que tiene un estado pending y un valor undefined:

Output
__proto__: Promise [[PromiseStatus]]: "pending" [[PromiseValue]]: undefined

Hasta ahora, no se ha configurado nada para la promesa, así que va a permanecer en un estado pending para siempre. Lo primero que puedes hacer para probar una promesa es cumplirla resolviéndola con un valor:

const promise = new Promise((resolve, reject) => {
  resolve('We did it!')
})

Ahora, al inspeccionar la promesa, encontrarás que tiene un estado de fulfilled, y un value establecido en el valor que pasaste a resolve:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: "We did it!"

Como se indicó al principio de esta sección, una promesa es un objeto que puede devolver un valor. Después de cumplirse con éxito, el value pasa de ser undefined a estar poblado con datos.

A promise can have three possible states: pending, fulfilled, and rejected.

  • Pendiente – Estado inicial antes de ser resuelta o rechazada
  • Cumplido – Operación exitosa, promesa ha sido resuelta
  • Rechazado – Operación fallida, promesa ha sido rechazada

Después de ser cumplida o rechazada, una promesa está resuelta.

Ahora que tienes una idea de cómo se crean las promesas, veamos cómo un desarrollador puede consumirlas.

Consumiendo una Promesa

La promesa en la última sección se ha cumplido con un valor, pero también deseas poder acceder al valor. Las promesas tienen un método llamado then que se ejecutará después de que una promesa alcance resolve en el código. then devolverá el valor de la promesa como parámetro.

Así es como devolverías y registrarías el valor de la promesa de ejemplo:

promise.then((response) => {
  console.log(response)
})

La promesa que creaste tenía un [[PromiseValue]] de ¡Lo logramos!. Este valor es el que se pasará a la función anónima como respuesta:

Output
We did it!

Hasta ahora, el ejemplo que creaste no implicaba una API web asíncrona, solo explicaba cómo crear, resolver y consumir una promesa nativa de JavaScript. Usando setTimeout, puedes probar una solicitud asíncrona.

El siguiente código simula datos devueltos de una solicitud asíncrona como una promesa:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Resolving an asynchronous request!'), 2000)
})

// Registrar el resultado
promise.then((response) => {
  console.log(response)
})

Usar la sintaxis then asegura que la respuesta se registre solo cuando la operación setTimeout se complete después de 2000 milisegundos. Todo esto se hace sin anidar devoluciones de llamada.

Ahora, después de dos segundos, resolverá el valor de la promesa y se registrará en then:

Output
Resolving an asynchronous request!

Las promesas también pueden encadenarse para pasar datos a más de una operación asincrónica. Si se devuelve un valor en then, se puede agregar otro then que se cumplirá con el valor de retorno del then anterior:

// Encadenar una promesa
promise
  .then((firstResponse) => {
    // Devolver un nuevo valor para el siguiente then
    return firstResponse + ' And chaining!'
  })
  .then((secondResponse) => {
    console.log(secondResponse)
  })

La respuesta cumplida en el segundo then registrará el valor de retorno:

Output
Resolving an asynchronous request! And chaining!

Dado que then puede encadenarse, permite que el consumo de promesas parezca más sincrónico que las devoluciones de llamada, ya que no es necesario anidarlas. Esto permitirá un código más legible que pueda mantenerse y verificarse más fácilmente.

Manejo de errores

Hasta ahora, solo has manejado una promesa con un resolve exitoso, lo que coloca la promesa en un estado fulfilled. Pero frecuentemente, con una solicitud asíncrona, también tienes que manejar un error: si la API está caída, o se envía una solicitud malformada o no autorizada. Una promesa debería poder manejar ambos casos. En esta sección, crearás una función para probar tanto el caso de éxito como el de error al crear y consumir una promesa.

Esta función getUsers pasará un indicador a una promesa y devolverá la promesa:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Manejar el resolve y el reject en la API asíncrona
    }, 1000)
  })
}

Configura el código para que si onSuccess es true, el tiempo de espera se cumpla con algunos datos. Si es false, la función rechazará con un error:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Manejar el resolve y el reject en la API asíncrona
      if (onSuccess) {
        resolve([
          {id: 1, name: 'Jerry'},
          {id: 2, name: 'Elaine'},
          {id: 3, name: 'George'},
        ])
      } else {
        reject('Failed to fetch data!')
      }
    }, 1000)
  })
}

Para el resultado exitoso, devuelve objetos JavaScript que representan datos de usuario de muestra.

Para manejar el error, usarás el método de instancia catch. Esto te dará un callback de fallo con el error como parámetro.

Ejecuta el comando getUser con onSuccess establecido en false, utilizando el método then para el caso de éxito y el método catch para el error:

// Ejecutar la función getUsers con el indicador false para provocar un error
getUsers(false)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

Desde que se desencadenó el error, se omitirá el then y el catch manejará el error:

Output
Failed to fetch data!

Si cambias la bandera y en su lugar resolve, el catch será ignorado y en su lugar se devolverá los datos:

// Ejecutar la función getUsers con la bandera true para resolver correctamente
getUsers(true)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

Esto proporcionará los datos del usuario:

Output
(3) [{…}, {…}, {…}] 0: {id: 1, name: "Jerry"} 1: {id: 2, name: "Elaine"} 3: {id: 3, name: "George"}

Para referencia, aquí hay una tabla con los métodos de manejo en objetos Promise:

Method Description
then() Handles a resolve. Returns a promise, and calls onFulfilled function asynchronously
catch() Handles a reject. Returns a promise, and calls onRejected function asynchronously
finally() Called when a promise is settled. Returns a promise, and calls onFinally function asynchronously

Las Promesas pueden ser confusas, tanto para nuevos desarrolladores como para programadores experimentados que nunca han trabajado en un entorno asíncrono. Sin embargo, como se mencionó, es mucho más común consumir promesas que crearlas. Por lo general, una API web del navegador o una biblioteca de terceros proporcionarán la promesa, y solo necesitas consumirla.

En la sección final sobre promesas, este tutorial citará un caso de uso común de una API web que devuelve promesas: la API Fetch.

Usando la API Fetch con Promesas

Una de las APIs web más útiles y frecuentemente utilizadas que devuelve una promesa es la Fetch API, que te permite realizar una solicitud de recurso asíncrona a través de una red. `fetch` es un proceso de dos partes y, por lo tanto, requiere encadenar `then`. Este ejemplo demuestra cómo obtener datos de usuario de la API de GitHub, mientras maneja cualquier posible error:

// Obtener un usuario de la API de GitHub
fetch('https://api.github.com/users/octocat')
  .then((response) => {
    return response.json()
  })
  .then((data) => {
    console.log(data)
  })
  .catch((error) => {
    console.error(error)
  })

La solicitud `fetch` se envía a la URL `https://api.github.com/users/octocat`, que espera de manera asíncrona una respuesta. El primer `then` pasa la respuesta a una función anónima que formatea la respuesta como datos JSON, luego pasa el JSON a un segundo `then` que registra los datos en la consola. La declaración `catch` registra cualquier error en la consola.

Al ejecutar este código, se obtendrá lo siguiente:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

Estos son los datos solicitados desde `https://api.github.com/users/octocat`, representados en formato JSON.

Esta sección del tutorial mostró que las promesas incorporan muchas mejoras para lidiar con código asíncrono. Sin embargo, aunque usar `then` para manejar acciones asíncronas es más fácil de seguir que la pirámide de devoluciones de llamada, algunos desarrolladores aún prefieren un formato síncrono para escribir código asíncrono. Para abordar esta necesidad, ECMAScript 2016 (ES7) introdujo funciones `async` y la palabra clave `await` para facilitar el trabajo con promesas.

Funciones asíncronas con async/await

Una función async te permite manejar código asíncrono de manera que parece sincrónico. Las funciones async siguen utilizando promesas internamente, pero tienen una sintaxis más tradicional de JavaScript. En esta sección, probarás ejemplos de esta sintaxis.

Puedes crear una función async agregando la palabra clave async antes de una función:

// Crea una función async
async function getUser() {
  return {}
}

Aunque esta función aún no maneja nada asíncrono, se comporta de manera diferente a una función tradicional. Si ejecutas la función, verás que devuelve una promesa con un [[PromiseStatus]] y [[PromiseValue]] en lugar de un valor de retorno.

Prueba esto registrando una llamada a la función getUser:

console.log(getUser())

Esto te dará lo siguiente:

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

Esto significa que puedes manejar una función async con then de la misma manera que podrías manejar una promesa. Pruébalo con el siguiente código:

getUser().then((response) => console.log(response))

Esta llamada a getUser pasa el valor de retorno a una función anónima que registra el valor en la consola.

Recibirás lo siguiente cuando ejecutes este programa:

Output
{}

Una función async puede manejar una promesa llamada dentro de ella utilizando el operador await. await se puede usar dentro de una función async y esperará hasta que una promesa se resuelva antes de ejecutar el código designado.

Con este conocimiento, puedes reescribir la solicitud Fetch de la última sección usando async/await de la siguiente manera:

// Manejar fetch con async/await
async function getUser() {
  const response = await fetch('https://api.github.com/users/octocat')
  const data = await response.json()

  console.log(data)
}

// Ejecutar función async
getUser()

Los operadores await aquí aseguran que los datos no se registren antes de que la solicitud los haya poblado con datos.

Ahora los datos finales pueden manejarse dentro de la función getUser, sin necesidad de usar then. Este es el resultado de registrar data:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

Nota: En muchos entornos, es necesario usar async para utilizar await, sin embargo, algunas nuevas versiones de navegadores y Node permiten usar await de nivel superior, lo que te permite evitar crear una función async para envolver el await.

Finalmente, dado que estás manejando la promesa cumplida dentro de la función asíncrona, también puedes manejar el error dentro de la función. En lugar de usar el método catch con then, usarás el patrón try/catch para manejar la excepción.

Agrega el siguiente código resaltado:

// Manejo de éxito y errores con async/await
async function getUser() {
  try {
    // Manejar éxito en try
    const response = await fetch('https://api.github.com/users/octocat')
    const data = await response.json()

    console.log(data)
  } catch (error) {
    // Manejar error en catch
    console.error(error)
  }
}

El programa ahora saltará al bloque catch si recibe un error y registrará ese error en la consola.

El código asincrónico moderno en JavaScript se maneja más a menudo con la sintaxis async/await, pero es importante tener un conocimiento funcional de cómo funcionan las promesas, especialmente porque las promesas son capaces de características adicionales que no se pueden manejar con async/await, como combinar promesas con Promise.all().

Nota: async/await se puede reproducir usando generadores combinados con promesas para agregar más flexibilidad a tu código. Para aprender más, consulta nuestro tutorial Entendiendo Generadores en JavaScript.

Conclusión

Porque las API web a menudo proporcionan datos de forma asincrónica, aprender a manejar el resultado de acciones asíncronas es una parte esencial de ser un desarrollador de JavaScript. En este artículo, aprendiste cómo el entorno de alojamiento utiliza el bucle de eventos para manejar el orden de ejecución del código con la pila y la cola. También probaste ejemplos de tres formas de manejar el éxito o el fracaso de un evento asíncrono, con devoluciones de llamada, promesas y sintaxis async/await. Finalmente, utilizaste la API web Fetch para manejar acciones asíncronas.

Para obtener más información sobre cómo el navegador maneja eventos paralelos, lee Modelo de concurrencia y el bucle de eventos en la Red de Desarrolladores de Mozilla. Si deseas aprender más sobre JavaScript, regresa a nuestra serie Cómo Programar en JavaScript.

Source:
https://www.digitalocean.com/community/tutorials/understanding-the-event-loop-callbacks-promises-and-async-await-in-javascript