Comprender Event Loop, Callbacks, Promises, y Async/Await en JavaScript
INFO
La fuente original (en ingles) de este tutorial se encuentra aquí
Introducción
En los primeros tiempos de Internet, los sitios web solían constar de 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 realizar solicitudes de red externas para recuperar datos de API. Para manejar estas operaciones en JavaScript, un desarrollador debe utilizar técnicas de programación asincrónica.
Dado que JavaScript es un lenguaje de programación de un solo hilo con un modelo de ejecución sincrónica que procesa una operación tras otra, solo puede procesar una instrucción a la vez. Sin embargo, una acción como solicitar datos de una API puede llevar una cantidad de tiempo indeterminada, según el 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 sincrónica, 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 el comportamiento de bloqueo, el entorno del navegador tiene muchas API 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 continuar usando el navegador normalmente mientras se procesan las operaciones asincrónicas.
Como desarrollador de JavaScript, debe saber cómo trabajar con API web asincrónicas y manejar la respuesta o el error de esas operaciones. En este artículo, aprenderá sobre el bucle de eventos, la forma original de lidiar con el comportamiento asincrónico a través de devoluciones de llamadas, la incorporación actualizada de promesas en ECMAScript 2015 y la práctica moderna de usar async/await
.
Nota
Este artículo se centra en JavaScript del lado del cliente en el entorno del navegador. Los mismos conceptos son generalmente válidos en el entorno de Node.js, sin embargo, Node.js utiliza sus propias C++ API en lugar de las Web API del navegador. Para obtener más información sobre la programación asincrónica en Node.js, consulte Cómo Escribir Código Asincrónico en Node.js.
El Bucle Event
En esta sección se explicará cómo JavaScript maneja el código asincrónico con el bucle de eventos. Primero se mostrará una demostración del bucle de eventos en funcionamiento 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 asincrónica se ejecutará de manera sincrónica, una a la vez, de manera secuencial. Esto se demuestra con este código de ejemplo que llama a tres funciones, cada una de las cuales imprime un número en la consola:
// Define three example functions
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:
// Execute the functions
first()
second()
third()
La salida se basará en el orden en que se llamaron las funciones—first()
, second()
y luego third()
:
Output
1
2
3
Cuando se utiliza una API web asincrónica, 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 período de tiempo específico. setTimeout
debe ser asincrónico, de lo contrario, todo el navegador permanecería congelado durante la espera, lo que daría como resultado una mala experiencia de usuario.
Agrega setTimeout
a la función second
para simular una solicitud asincrónica:
// Define three example functions, but one of them contains asynchronous code
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 asincrónica 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 configuraste la función para que se ejecute después de 0
milisegundos.
Ahora llama a las funciones, como lo hiciste antes:
// Execute the functions
first()
second()
third()
Se podría esperar que con un setTimeout
establecido en 0
, la ejecución de estas tres funciones dé como resultado que los números se impriman en orden secuencial. Pero debido a que es asincrónico, la función con el tiempo de espera se imprimirá en último lugar:
Output
1
3
2
No habrá diferencia alguna si establece el tiempo de espera en cero segundos o cinco minutos—el console.log
llamado por el código asincrónico se ejecutará después de las funciones de nivel superior sincrónicas. Esto sucede porque el entorno de host de JavaScript, en este caso el navegador, utiliza un concepto llamado 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 esté 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
La pila, o pila de llamadas, contiene el estado de la función que se está ejecutando en ese momento. Si no estás familiarizado con el concepto de pila, puedes imaginarla como una matriz con propiedades de último en entrar, primero en salir (LIFO, “Last in, first out”), lo que significa que solo puedes agregar o quitar elementos del final de la pila. JavaScript ejecutará el marco actual (o la llamada de función en un entorno específico) en la pila, luego lo eliminará y pasará al siguiente.
Para el ejemplo que solo contiene código sincrónico, el navegador maneja la ejecución en el siguiente orden:
- Agrega
first()
a la pila, ejecutafirst()
que registra1
en la consola, eliminafirst()
de la pila. - Agrega
second()
a la pila, ejecutasecond()
que registra2
en la consola, eliminasecond()
de la pila. - Agrega
third()
a la pila, ejecutathird()
que registra3
en la consola, eliminathird()
de la pila.
El segundo ejemplo con setTimeout
se ve así:
- Agrega
first()
a la pila, ejecutafirst()
que registra1
en la consola, eliminafirst()
de la pila. - Agrega
second()
a la pila, ejecutasecond()
.- Agrega
setTimeout()
a la pila, ejecute la API WebsetTimeout()
que inicia un temporizador y agrega la función anónima a la cola, eliminasetTimeout()
de la pila.
- Agrega
- Elimina
second()
de la pila. - Agrega
third()
a la pila, ejecutathird()
que registra3
en la consola, eliminathird()
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 registra2
en la consola y luego la elimina de la pila.
El uso de setTimeout
, una API Web asincrónica, presenta el concepto de 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. Siempre que la pila de llamadas esté vacía, el bucle de eventos comprobará la cola en busca de mensajes en espera, comenzando por el mensaje más antiguo. Una vez que encuentre uno, lo agregará a la pila, que ejecutará la función en el mensaje.
En el ejemplo 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 ese lapso de tiempo. Este sistema de cola existe porque si el temporizador agregara la función anónima directamente a la pila cuando finaliza, interrumpiría cualquier función que se esté ejecutando en ese momento, lo que podría tener efectos no deseados e impredecibles.
Nota
También hay otra cola llamada cola de trabajos o cola de microtareas que maneja promesas. Las microtareas como las promesas se manejan con mayor prioridad que las macrotareas como setTimeout
.
Ahora ya sabe cómo el bucle de eventos utiliza la pila y la cola para manejar el orden de ejecución del código. La siguiente tarea es averiguar cómo controlar el orden de ejecución en su código. Para ello, primero aprenderá sobre la forma original de garantizar que el bucle de eventos maneje correctamente el código asincrónico: las funciones de devolución de llamada.
Funciones de Devolución de Llamada
En el ejemplo setTimeout
, la función con el tiempo de espera se ejecutó después de todo lo que se encontraba en el contexto de ejecución principal de nivel superior. Pero si desea asegurarse de que una de las funciones, como la función third
, se ejecutara después del tiempo de espera, entonces tendría que usar métodos de codificación asincrónica. El tiempo de espera aquí puede representar una llamada API asincrónica que contiene datos. Desea trabajar con los datos de la llamada API, pero debe asegurarse de que los datos se devuelvan primero.
La solución original para tratar este problema es usar funciones de devolución de llamada. Las funciones de devolución de llamada no tienen una sintaxis especial; son simplemente una función que se ha pasado como argumento a otra función. La función que toma otra función como argumento se denomina 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 asincrónicas por naturaleza, pero se pueden utilizar para fines asincrónicos.
A continuación se muestra un ejemplo de código sintáctico de una función de orden superior y una devolución de llamada:
// A function
function fn() {
console.log('Just a function')
}
// A function that takes another function as an argument
function higherOrderFunction(callback) {
// When you call a function that is passed as an argument, it is referred to as a callback
callback()
}
// Passing a function
higherOrderFunction(fn)
En este código, se define una función fn
, se define una función higherOrderFunction
que toma un callback
de función como argumento y se pasa fn
como una devolución de llamada a higherOrderFunction
.
Si se ejecuta este código, se obtendrá lo siguiente:
Output
Just a function
Volvamos a las funciones first
, second
y third
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 consiste en lograr que la función third
siempre retrase la ejecución hasta que se haya completado la acción asincrónica en la función second
. Aquí es donde entran en juego las devoluciones de llamadas. En lugar de ejecutar first
, second
y third
en el nivel superior de ejecución, pasará la función third
como argumento a second
. La función second
ejecutará la devolución de llamada después de que se haya completado la acción asincrónica.
A continuación, se muestran las tres funciones con una devolución de llamada aplicada:
// Define three functions
function first() {
console.log(1)
}
function second(callback) {
setTimeout(() => {
console.log(2)
// Execute the callback function
callback()
}, 0)
}
function third() {
console.log(3)
}
Ahora, ejecute first
y second
, luego pase third
como argumento a second
:
first()
second(third)
Después de ejecutar este bloque de código, recibirá el siguiente resultado:
Output
1
2
3
Primero se imprimirá 1
y, una vez que se complete el temporizador (en este caso, cero segundos, pero puede cambiarlo a cualquier cantidad), imprimirá 2
y luego 3
. Al pasar una función como devolución de llamada, ha retrasado con éxito la ejecución de la función hasta que se complete la API Web asincrónica (setTimeout
).
La conclusión clave aquí es que las funciones de devolución de llamada no son asincrónicas—setTimeout
es la API Web asincrónica responsable de gestionar las tareas asincrónicas. La devolución de llamada solo le permite recibir información sobre cuándo se ha completado una tarea asincrónica y gestiona el éxito o el fracaso de la tarea.
Ahora que ha aprendido a utilizar las devoluciones de llamadas para gestionar tareas asincrónicas, la siguiente sección explica los problemas de anidar demasiadas devoluciones de llamadas y crear una "pirámide de la perdición".
Devoluciones de Llamada anidadas y la Pirámide de la Perdición
Las funciones de devolución de llamadas son una forma eficaz de garantizar la ejecución retrasada de una función hasta que otra se complete y regrese con datos. Sin embargo, debido a la naturaleza anidada de las devoluciones de llamadas, el código puede terminar volviéndose desordenado si tiene muchas solicitudes asincrónicas consecutivas que dependen unas de otras. Esto fue una gran frustración para los desarrolladores de JavaScript al principio y, como resultado, el código que contiene devoluciones de llamadas anidadas a menudo se denomina la "pirámide de la perdición" o "el infierno de las devoluciones de llamadas".
A continuación, se muestra una demostración de devoluciones de llamadas 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
se anida dentro de una función de orden superior, lo que crea una forma piramidal de devoluciones de llamadas cada vez más profundas. Al ejecutar este código, obtendríamos lo siguiente:
Output
1
2
3
En la práctica, con el código asincrónico del mundo real, esto puede volverse mucho más complicado. Lo más probable es que necesites manejar errores en código asincrónico y luego pasar algunos datos de cada respuesta a la siguiente solicitud. Hacer esto con devoluciones de llamadas hará que tu código sea difícil de leer y mantener.
A continuación, se muestra un ejemplo ejecutable de una “pirámide de la perdición” más realista con la que puedes experimentar:
// Example asynchronous function
function asynchronousRequest(args, callback) {
// Throw an error if no arguments are passed
if (!args) {
return callback(new Error('Whoa! Something went wrong.'))
} else {
return setTimeout(
// Just adding in a random number so it seems like the contrived asynchronous function
// returned different data
() => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
500,
)
}
}
// Nested asynchronous requests
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)
})
})
})
}
// Execute
callbackHell()
En este código, debes hacer que cada función tenga en cuenta un posible response
y un posible error
, lo que hace que la función callbackHell
sea visualmente confusa.
Si ejecutas 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 el código asincrónico es difícil de seguir. Por ello, en ES6 se introdujo el concepto de promesas. Este es el tema central de la siguiente sección.
Promesas
Una promesa representa la finalización de una función asincrónica. Es un objeto que podría devolver un valor en el futuro. Cumple el mismo objetivo básico que una función de devolución de llamada, pero con muchas características adicionales y una sintaxis más legible. Como desarrollador de JavaScript, es probable que pase más tiempo consumiendo promesas que creándolas, ya que generalmente son las API web asincrónicas las que devuelven una promesa para que el desarrollador la consuma. Este tutorial le mostrará cómo hacer ambas cosas.
Creando una Promesa
Puede inicializar una promesa con la sintaxis new Promise
y debe 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.
Escriba la siguiente línea para declarar una promesa:
// Initialize a promise
const promise = new Promise((resolve, reject) => {})
Si inspecciona la promesa inicializada en este estado con la consola de su navegador web, encontrará 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, por lo que permanecerá en un estado pending
para siempre. Lo primero que puede 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á que tiene un estado fulfilled
y un value
establecido en el valor que pasó para 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 correctamente, el value
pasa de undefined
a estar lleno de datos.
Una promesa puede tener tres estados posibles: pending, fulfilled y rejected.
- Pending - (Pendiente) Estado inicial antes de ser resuelta o rechazada
- Fulfilled - (Cumplida) Operación exitosa, la promesa se resolvió
- Rejected - (Rechazada) Operación fallida, la promesa se rechazó
Después de ser cumplida o rechazada, una promesa se liquida.
Ahora que tiene una idea de cómo se crean las promesas, veamos cómo un desarrollador puede consumirlas.
Consumiendo una Promesa
La promesa de la última sección se ha cumplido con un valor, pero también quieres poder acceder a ese 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 value
de la promesa de ejemplo:
promise.then((response) => {
console.log(response)
})
La promesa que creaste tenía un [[PromiseValue]]
de We did it!
. Este valor es el que se pasará a la función anónima como response
:
Output
We did it!
Hasta ahora, el ejemplo que creaste no involucraba una API Web asincrónica — solo explicaba cómo crear, resolver y consumir una promesa de JavaScript nativa. Con setTimeout
, puedes probar una solicitud asincrónica.
El siguiente código simula los datos devueltos de una solicitud asincrónica como una promesa:
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('Resolving an asynchronous request!'), 2000)
})
// Log the result
promise.then((response) => {
console.log(response)
})
El uso de la sintaxis then
garantiza que el response
se registrará solo cuando la operación setTimeout
se complete después de 2000
milisegundos. Todo esto se hace sin anidar devoluciones de llamadas.
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 se pueden encadenar 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 cumpla con el valor de retorno del then
anterior:
// Chain a promise
promise
.then((firstResponse) => {
// Return a new value for the next 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
se puede encadenar, permite que el consumo de promesas parezca más sincrónico que las devoluciones de llamadas, ya que no necesitan estar anidadas. Esto permitirá un código más legible que se pueda mantener y verificar más fácilmente.
Manejo de Errores
Hasta ahora, solo ha manejado una promesa con una resolve
exitosa, que pone la promesa en un estado fulfilled
. Pero con frecuencia con una solicitud asincrónica también tiene que manejar un error — si la API está inactiva o se envía una solicitud mal formada o no autorizada. Una promesa debería poder manejar ambos casos. En esta sección, creará una función para probar tanto el caso de éxito como el de error de la creación y el consumo de 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(() => {
// Handle resolve and reject in the asynchronous API
}, 1000)
})
}
Configura el código de modo que si onSuccess
es true
, el tiempo de espera se cumplirá con algunos datos. Si es false
, la función rechazará la operación con un error:
function getUsers(onSuccess) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// Handle resolve and reject in the asynchronous API
if (onSuccess) {
resolve([
{id: 1, name: 'Jerry'},
{id: 2, name: 'Elaine'},
{id: 3, name: 'George'},
])
} else {
reject('Failed to fetch data!')
}
}, 1000)
})
}
Para obtener un resultado satisfactorio, se devuelven objetos JavaScript que representan datos de usuario de muestra.
Para manejar el error, utilizará el método de instancia catch
. Esto le proporcionará una devolución de llamada de error con error
como parámetro.
Ejecute 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:
// Run the getUsers function with the false flag to trigger an error
getUsers(false)
.then((response) => {
console.log(response)
})
.catch((error) => {
console.error(error)
})
Dado que se desencadenó el error, se omitirá el then
y el catch
manejará el error:
Output
Failed to fetch data!
Si cambia la bandera y resolve
en su lugar, se ignorará catch
y los datos regresarán en su lugar:
// Run the getUsers function with the true flag to resolve successfully
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"}
Como referencia, aquí hay una tabla con los métodos de manejo sobre objetos Promise
:
Método | Descripción |
---|---|
then() | Maneja un resolve . Devuelve una promesa y llama a la función onFulfilled de forma asincrónica. |
catch() | Maneja un reject . Devuelve una promesa y llama a la función onRejected de forma asincrónica. |
finally() | Se llama cuando se cumple una promesa. Devuelve una promesa y llama a la función onFinally de forma asincrónica. |
Las promesas pueden ser confusas, tanto para los nuevos desarrolladores como para los programadores experimentados que nunca han trabajado en un entorno asincrónico. Sin embargo, como se mencionó, es mucho más común consumir promesas que crearlas. Por lo general, la API Web de un navegador o una biblioteca de terceros proporcionará la promesa y solo es necesario consumirla.
En la sección final de 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 API Web más útiles y utilizadas que devuelve una promesa es la API Fetch, que permite realizar una solicitud de recursos asincrónica 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 acceder a la API de GitHub para obtener los datos de un usuario y, al mismo tiempo, gestionar cualquier error potencial:
// Fetch a user from the GitHub API
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 una respuesta de forma asincrónica. El primer then
pasa la respuesta a una función anónima que formatea la respuesta como datos JSON y 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 manejar código asincrónico. Pero, si bien usar then
para manejar acciones asincrónicas es más fácil de seguir que la pirámide de devoluciones de llamadas, algunos desarrolladores aún prefieren un formato sincrónico para escribir código asincrónico. Para abordar esta necesidad, ECMAScript 2016 (ES7) introdujo funciones async
y la palabra clave await
para facilitar el trabajo con promesas.
Funciones Asincrónicas con async/await
Una función async
le permite manejar código asincrónico de una manera que parezca sincrónica. Las funciones async
aún usan promesas en segundo plano, pero tienen una sintaxis de JavaScript más tradicional. En esta sección, probará ejemplos de esta sintaxis.
Puede crear una función async
agregando la palabra clave async
antes de una función:
// Create an async function
async function getUser() {
return {}
}
Aunque esta función aún no maneja nada asincrónico, se comporta de manera diferente a una función tradicional. Si ejecuta la función, verá que devuelve una promesa con un [[PromiseStatus]]
y [[PromiseValue]]
en lugar de un valor de retorno.
Pruebe esto registrando una llamada a la función getUser
:
console.log(getUser())
Esto dará como resultado 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 manejarías una promesa. Prueba esto con el siguiente código:
getUser().then((response) => console.log(response))
Esto llamada a getUser
pasa el valor de retorno a una función anónima que registra el valor en la consola.
Recibirá lo siguiente cuando ejecute este programa:
Output
{}
Una función async
puede manejar una promesa llamada dentro de ella usando el operador await
. await
se puede usar dentro de una función async
y esperará hasta que se cumpla una promesa antes de ejecutar el código designado.
Con este conocimiento, puede reescribir la solicitud Fetch de la última sección usando async
/await
de la siguiente manera:
// Handle fetch with async/await
async function getUser() {
const response = await fetch('https://api.github.com/users/octocat')
const data = await response.json()
console.log(data)
}
// Execute async function
getUser()
Los operadores await
aquí garantizan que data
no se registre antes de que la solicitud se haya llenado con datos.
Ahora, la data
final se pueden manejar dentro de la función getUser
, sin necesidad de usar then
. Este es el resultado del registro de 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 async
para usar await
— sin embargo, algunas versiones nuevas de navegadores y Node permiten usar await
de nivel superior, lo que permite evitar la creación de una función asíncrona para envolver await
.
Por último, dado que estás manejando la promesa cumplida dentro de la función asincrónica, 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:
// Handling success and errors with async/await
async function getUser() {
try {
// Handle success in try
const response = await fetch('https://api.github.com/users/octocat')
const data = await response.json()
console.log(data)
} catch (error) {
// Handle error in 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 JavaScript asincrónico moderno se maneja con mayor frecuencia con la sintaxis async
/await
, pero es importante tener un conocimiento práctico de cómo funcionan las promesas, especialmente porque las promesas son capaces de ofrecer funciones adicionales que no se pueden manejar con async
/await
, como combinar promesas con Promise.all()
.
Nota
async
/await
se puede reproducir mediante el uso de generadores combinados con promesas para agregar más flexibilidad a su código. Para obtener más información, consulte nuestro tutorial Comprender Generadores en JavaScript.
Conclusión
Dado que las API Web suelen proporcionar datos de forma asincrónica, aprender a manejar el resultado de acciones asincrónicas es una parte esencial de ser un desarrollador de JavaScript. En este artículo, aprendiste cómo el entorno host usa 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 asincrónico, con devoluciones de llamadas, promesas y sintaxis async
/await
. Finalmente, usaste la API Web Fetch para manejar acciones asincrónicas.
Para obtener más información sobre cómo el navegador maneja eventos paralelos, lee Modelo de concurrencia y bucle de eventos en Mozilla Developer Network. Si deseas obtener más información sobre JavaScript, vuelve a nuestra serie Cómo codificar en JavaScript.