Skip to content
This page has been auto-translated and may contain errors.View in English

JavaScript Asincrónico

docs.scrimba.com

Algunas cosas suceden instantáneamente: sumar dos números, leer una variable, cambiar texto en la página. Otras toman tiempo. Un temporizador cuenta regresivamente durante tres segundos. Una solicitud a un servidor viaja por la red y regresa un momento después. Si JavaScript se detuviera y esperara por cada una de esas, toda la página se congelaría: sin clics, sin desplazamiento, nada, hasta que la cosa lenta terminara. No funciona así. Este capítulo trata sobre cómo JavaScript inicia algo lento, mantiene la página receptiva, y regresa para manejar el resultado cuando está listo.

Por qué async

La mayoría del JavaScript se ejecuta de arriba a abajo, una línea terminando antes de que la siguiente comience. Eso está bien para el trabajo rápido. Pero algunas tareas toman tiempo real, y JavaScript no se sienta y espera por ellas. Inicia la tarea lenta, se mueve a la siguiente línea, y regresa a la lenta cuando esté hecha. Eso es lo que significa asincrónico: no sucediendo en orden, no ahora mismo, pero después, cuando el resultado está listo.

El ejemplo más simple es un temporizador. setTimeout ejecuta un código después de un retraso:

js
console.log("Inicio");
setTimeout(() => {
  console.log("3 segundos después");
}, 3000);
console.log("Fin");
// Inicio
// Fin
// 3 segundos después

Nota el orden. "Fin" se imprime antes de "3 segundos después", aunque viene después de setTimeout en el código. JavaScript no esperó tres segundos en esa línea. Estableció el temporizador, siguió adelante, y ejecutó el código retrasado cuando el tiempo se agotó.

JavaScript ejecuta una cosa a la vez, de arriba a abajo. Ese modelo funciona hasta que una tarea toma tiempo: un temporizador, una solicitud de red, leer un archivo. Si el lenguaje se bloqueara en esos, la página se congelaría mientras esperaba, porque nada más podría ejecutarse. El código asincrónico es la respuesta: comienzas la tarea lenta, JavaScript sigue ejecutando el resto de tu código, y el resultado se maneja después cuando llega.

setTimeout es el ejemplo inicial más claro. Le das una función y un retraso en milisegundos, y ejecuta esa función después del retraso:

js
console.log("Inicio");
setTimeout(() => {
  console.log("3 segundos después");
}, 3000);
console.log("Fin");
// Inicio
// Fin
// 3 segundos después

La función que pasas a setTimeout es un callback: código que entregas para ser llamado después, no ahora. JavaScript registra el temporizador, se mueve directamente a "Fin", y solo ejecuta el callback una vez que el retraso se cumple y el código actual ha terminado. Ese patrón de "termina ahora, regresa después" es toda la idea del async, y todo lo demás en este capítulo se basa en esto.

JavaScript es de un solo hilo: una pila de llamadas, una cosa ejecutándose a la vez. Esa restricción es exactamente por qué el async importa. Una espera bloqueante en una solicitud de red estancaría el único hilo, y con él cada clic, animación y renderizado en la página. Así que el trabajo lento no se hace en línea. Se entrega al runtime (el navegador o Node), que hace la espera fuera de tu hilo y programa tu código para que se reanude cuando el resultado esté listo. El código asincrónico es esa entrega: describes qué hacer con un resultado que aún no tienes.

setTimeout es el caso minimal, y expone el modelo limpiamente:

js
console.log("Inicio");
setTimeout(() => {
  console.log("después");
}, 0);
console.log("Fin");
// Inicio
// Fin
// después

El retraso aquí es 0, y el callback aún se ejecuta último. Ese es el indicador: los callbacks async nunca se ejecutan en medio del código sincrónico. setTimeout no ejecuta su callback después de cero milisegundos, programa el callback para que se ejecute después de que el código sincrónico actual se agote. El mecanismo detrás de ese ordenamiento es el event loop, que la última sección de este capítulo desglosa. Por ahora, mantén la regla: el código sincrónico se ejecuta hasta completarse primero, luego el trabajo async en cola.

JunoPor qué async Algunas tareas toman tiempo, como un temporizador o cargar datos, y JavaScript no se congela mientras espera. Inicia la cosa lenta, sigue ejecutando las siguientes líneas, y regresa a la cosa lenta cuando está lista. Por eso "Fin" se imprime antes del mensaje de setTimeout, aunque el temporizador viene primero en el código.
JunoPor qué async Async significa iniciar la tarea lenta ahora, manejar su resultado después, y mantener la página receptiva entre tanto. setTimeout es la versión más simple: le entregas un callback y ejecuta ese callback después del retraso, no en el acto. Una vez que ves por qué el mensaje retrasado se imprime último, el resto de este capítulo son variaciones de esa una idea.
JunoPor qué async JavaScript es de un solo hilo, así que bloquearse en trabajo lento estancaría todo. En su lugar entregas la espera al runtime y describes qué hacer con el resultado cuando llegue. El caso setTimeout(fn, 0) es la pista reveladora: el callback aún se ejecuta después del código sincrónico, porque el trabajo async en cola espera a que el código actual termine primero.

Promesas

setTimeout está bien para un retraso, pero la mayoría del trabajo async produce un valor que te importa: los datos que solicitaste, o un error si algo salió mal. Para eso, JavaScript usa una promesa. Una promesa es un valor que aún no está listo, con un lugar reservado para él. Es como un recibo: no tienes la comida, pero tienes algo que se convertirá en la comida.

Una promesa termina de una de dos formas. Se resuelve con un valor cuando el trabajo tiene éxito, o rechaza con un error cuando falla. Dices qué hacer en cada caso con .then() y .catch():

js
loadUser()
  .then((user) => {
    console.log(`Cargado ${user.name}`);
  })
  .catch((error) => {
    console.log("Algo salió mal");
  });

.then() se ejecuta cuando la promesa se resuelve, y recibe el valor. .catch() se ejecuta si rechaza, y recibe el error. El código dentro de ellos se ejecuta después, una vez que el resultado llega.

Los callbacks funcionan, pero se apilan mal. Cuando una tarea async depende de otra, y esa de otra, terminas anidando callbacks dentro de callbacks, derivando más a la derecha con cada paso. Esa forma tiene un nombre, la "pirámide de perdición", y se vuelve difícil de leer y más difícil de manejar errores en. Una promesa arregla la forma.

Una promesa es un objeto que representa un valor que aún no está listo. Está en uno de tres estados: pendiente (aún funcionando), cumplida (resuelta con un valor), o rechazada (falló con un error). Adjuntas manejadores con .then() para el éxito y .catch() para el fallo:

js
loadUser()
  .then((user) => {
    console.log(`Cargado ${user.name}`);
    return loadPosts(user.id);
  })
  .then((posts) => {
    console.log(`Se encontraron ${posts.length} publicaciones`);
  })
  .catch((error) => {
    console.log(`Falló: ${error.message}`);
  });

Dos cosas hacen esto mejor que callbacks anidados. Primero, es plano: cada .then() devuelve una promesa, así que las encadenas en una línea recta en lugar de anidar. Devuelve un valor de un .then() y el siguiente lo recibe; devuelve una promesa y la cadena espera por ella. Segundo, un .catch() al final maneja un fallo en cualquier lugar de la cadena, así que escribes manejo de errores una vez en lugar de en cada nivel.

Antes de las promesas, los resultados async se entregaban a través de callbacks, y los pasos async dependientes significaban anidar callbacks dentro de callbacks. Más allá del costo de legibilidad, ese patrón no tiene una ruta de error unificada: cada nivel maneja su propio fallo, y olvidar uno silencia el error. Una promesa resuelve ambos problemas haciendo del resultado eventual un valor de primera clase que puedes pasar, encadenar, y manejar en un lugar.

Una promesa es un objeto en uno de tres estados: pendiente, cumplida con un valor, o rechazada con una razón. Una vez que se resuelve (cumplida o rechazada) nunca cambia de nuevo. Registras reacciones con .then(onFulfilled) y .catch(onRejected), y el encadenamiento es donde reside el verdadero poder:

js
loadUser()
  .then((user) => loadPosts(user.id)) // devuelve una promesa, la cadena espera por ella
  .then((posts) => posts.filter((post) => post.published))
  .then((published) => console.log(`${published.length} publicadas`))
  .catch((error) => console.log(`Falló: ${error.message}`));

Cada .then() devuelve una nueva promesa, y el valor de retorno del manejador la determina: devuelve un valor plano y el siguiente .then() recibe ese valor, devuelve una promesa y la cadena la adopta y espera. Un error lanzado o un rechazo en cualquier lugar salta los manejadores .then() restantes y salta al siguiente .catch(), que es por qué un manejador al final atrapa fallas de cualquier paso. Esta es una mejora real sobre callbacks, no solo una sintaxis más bonita: los errores se propagan automáticamente, y la cadena plana se lee en el orden en que se ejecuta. En la práctica leerás muchas cadenas de promesas, pero escribirás la mayoría de tu propio código async con async/await, que es la siguiente sección y se sienta encima de exactamente esta maquinaria.

JunoPromesas Una promesa es un valor que aún no está listo, como un recibo para un resultado que llega después. Se resuelve con un valor cuando el trabajo tiene éxito, o rechaza con un error cuando falla. Dices qué hacer con .then() para el valor y .catch() para el error, y ambos se ejecutan después una vez que el resultado está.
JunoPromesas Una promesa representa un resultado que aún viene, y supera los callbacks anidados porque encadenas .then() en una línea plana en lugar de derivar hacia la derecha. Devuelve un valor de un .then() y el siguiente lo obtiene, devuelve una promesa y la cadena espera. Un .catch() al final maneja un fallo en cualquier lugar de la cadena, así que escribes manejo de errores una vez.
JunoPromesas Una promesa es un objeto que se resuelve una vez, a cumplida o rechazada, y nunca cambia después. Cada .then() devuelve una nueva promesa, así que devolver un valor lo pasa adelante y devolver una promesa hace que la cadena espere, mientras que cualquier rechazo salta adelante al siguiente .catch(). Leerás muchas cadenas, pero escribirás la mayoría de tu async con async y await, que se sientan justo encima de esto.

async / await

Las cadenas de promesas funcionan, pero hay una forma más limpia de escribirlas que se lee casi como código ordinario de arriba a abajo. Utiliza dos palabras clave, async y await.

Marcas una función async, y dentro de ella puedes await una promesa. await pausa la función hasta que la promesa se resuelve, luego te da el valor directamente, sin necesidad de .then():

js
async function showUser() {
  const user = await loadUser();
  console.log(`Cargado ${user.name}`);
}

Lee eso de arriba a abajo: obtén el usuario, luego registra el nombre. La línea await espera a que loadUser() termine y te da el user. La función se pausa allí, pero el resto de la página sigue ejecutándose, así que nada se congela.

Para manejar errores, envuelve el await en try y catch:

js
async function showUser() {
  try {
    const user = await loadUser();
    console.log(`Cargado ${user.name}`);
  } catch (error) {
    console.log("No se pudo cargar el usuario");
  }
}

Si la promesa falla, el código salta al bloque catch en lugar de bloquearse.

async/await es la forma moderna de escribir código async. Se basa en promesas, así que nada debajo cambia, pero te permite escribir lógica async que se lee como el código sincrónico que ya conoces de funciones.

Marca una función async y puedes usar await dentro de ella. await toma una promesa, pausa la función hasta que se resuelve, y se evalúa al valor resuelto:

js
async function showUser() {
  const user = await loadUser();
  const posts = await loadPosts(user.id);
  console.log(`${user.name} tiene ${posts.length} publicaciones`);
}

Compara eso con la cadena .then() de la sección anterior: mismo trabajo, pero plano y lineal, con los valores aterrizando en bindings const planos que puedes usar en la siguiente línea. Una función async siempre devuelve una promesa en sí misma, así que llamar a showUser() te da una promesa que puedes await o .then() desde otro lugar.

Los errores son la parte que la gente pierde. Una promesa rechazada dentro de una función async lanza, así que la atrapas con un try/catch ordinario:

js
async function showUser() {
  try {
    const user = await loadUser();
    console.log(`Cargado ${user.name}`);
  } catch (error) {
    console.log(`Falló: ${error.message}`);
  }
}

El mismo try/catch que usarías para un error sincrónico maneja uno async, que es una gran parte de por qué async/await ganó.

async/await es sintaxis sobre promesas. Una función async siempre devuelve una promesa, y await desenvuelve una: suspende la función, cede el hilo de vuelta al runtime, y se reanuda con el valor resuelto cuando la promesa se resuelve. Nada está bloqueado mientras espera. La función está pausada, pero el único hilo está libre para ejecutar otro trabajo, que es todo el punto.

La recompensa es que el código async recupera las dos cosas que los callbacks y las cadenas raw te cuestan: orden de lectura lineal y flujo de control normal. await te deja usar const, condicionales, y bucles alrededor de valores async como si fueran sincrónico:

js
async function publishReport(userId) {
  const user = await loadUser(userId);
  if (!user.active) {
    return null; // el retorno temprano funciona normalmente
  }
  const posts = await loadPosts(user.id);
  return posts.filter((post) => post.published);
}

El manejo de errores se colapsa en try/catch porque una promesa awaited rechazada lanza en el await:

js
async function publishReport(userId) {
  try {
    const user = await loadUser(userId);
    return await loadPosts(user.id);
  } catch (error) {
    console.log(`Reporte falló: ${error.message}`);
    return [];
  }
}

Una sutileza que vale la pena interiorizar: return await dentro de un try importa. Si escribes return loadPosts(...) sin await, la función devuelve la promesa y sale del try antes de que la promesa se resuelva, así que un rechazo escapa del catch. Con await, el rechazo se lanza mientras aún estás dentro del try, y el catch lo ve. Ese es el tipo de detalle que se convierte en un bug "por qué mi manejador de errores no se disparó", y la siguiente sección sobre el event loop y patrones de error profundiza en esto.

Junoasync y await Marca una función async y puedes await una promesa dentro de ella. await pausa esa función hasta que la promesa se resuelve y te da el valor de inmediato, así que el código se lee de arriba a abajo sin .then(). Envuélvelo en try y catch para manejar fallas, y la página sigue ejecutándose mientras la función espera.
Junoasync y awaitasync/await es promesas con sintaxis más limpia: await pausa la función hasta que una promesa se resuelve y te da el valor en un const plano. Una función async siempre devuelve una promesa, así que puedes await desde otro lugar. Los errores se lanzan, así que un try/catch normal los maneja, y esa es la mayoría de por qué este estilo ganó.
Junoasync y awaitawait suspende la función y cede el hilo, luego se reanuda con el valor resuelto, así que nada se bloquea mientras esperas. Obtienes lectura lineal, flujo de control normal, y try/catch para errores. Ten cuidado con return await dentro de un try: quita el await y un rechazo escapa antes de que el catch pueda verlo.

Obtener datos

La tarea async más común que escribirás es cargar datos desde un servidor. El navegador te da fetch para esto. Le pasas una URL, y devuelve una promesa para la respuesta:

js
async function loadUsers() {
  const response = await fetch("https://api.example.com/users");
  const users = await response.json();
  console.log(users);
}

Hay dos await, y eso sorprende a la gente al principio. El primero espera a que el servidor responda. El segundo lee el cuerpo de esa respuesta y la convierte en datos de JavaScript con .json(), que en sí es async. Así que obtener datos es dos pasos: obtener la respuesta, luego leerla.

Los servidores también pueden responder con un error, como "no encontrado". fetch no trata eso como un fallo por sí solo, así que verificas response.ok tú mismo:

js
async function loadUsers() {
  const response = await fetch("https://api.example.com/users");
  if (!response.ok) {
    console.log("Solicitud falló");
    return;
  }
  const users = await response.json();
  console.log(users);
}

Cargar datos desde un servidor es la tarea async que más alcanzarás, y el fetch del navegador es cómo lo haces. Dale una URL y devuelve una promesa que se resuelve a un objeto Response:

js
async function loadUsers() {
  const response = await fetch("https://api.example.com/users");
  if (!response.ok) {
    throw new Error(`Solicitud falló: ${response.status}`);
  }
  const users = await response.json();
  return users;
}

La naturaleza de dos pasos es lo que hay que mantener. El primer await se resuelve cuando llegan los encabezados de la respuesta, dándote una Response. Esa respuesta aún no es los datos, es un identificador para ellos. Leer el cuerpo es un segundo paso async: response.json() devuelve una promesa que se resuelve a los datos analizados. Dos awaits, dos etapas.

La trampa que atrapa a todos: fetch solo rechaza en un fallo de red, no en un estado de error HTTP. Un 404 o 500 aún se resuelve; verifica response.ok tú mismo. response.ok es true para códigos de estado en los 200s, así que una guardia en !response.ok es cómo conviertes un estado malo en un error real. Una vez que lanzas allí, un try/catch alrededor de la llamada maneja tanto fallas de red como respuestas malas en un lugar.

fetch(url) es el cliente HTTP async del navegador, y devuelve una promesa que se resuelve a una Response. El diseño tiene un borde afilado que vale la pena declarar de frente: la promesa solo rechaza en un fallo a nivel de red, una conexión caída o una solicitud bloqueada. Un estado de error HTTP como 404 o 500 es un viaje redondo exitoso desde la perspectiva de fetch, así que se resuelve normalmente. Verificar response.ok no es opcional.

js
async function loadUsers() {
  const response = await fetch("https://api.example.com/users");
  if (!response.ok) {
    throw new Error(`Solicitud falló: ${response.status}`);
  }
  return await response.json();
}

La forma de dos etapas refleja streaming. El primer await se resuelve cuando los encabezados de la respuesta llegan, antes de que el cuerpo haya llegado necesariamente. La Response es un identificador para un stream, y response.json() lee ese stream hasta completarse y lo analiza, que es por qué en sí mismo es async y necesita su propio await. Convertir un estado malo en un error lanzado dentro de la función significa un try/catch en el sitio de la llamada cubre fallo de red, estado malo, y JSON que falla al analizar, tres modos de fallo diferentes dirigidos a un manejador:

js
try {
  const users = await loadUsers();
  render(users);
} catch (error) {
  console.log(`No se pudo cargar usuarios: ${error.message}`);
}

Esa es toda la anatomía práctica de un fetch: una promesa para la respuesta, una verificación de estado, una segunda promesa para el cuerpo, y un límite de error único alrededor de todo. La siguiente sección cubre qué sucede cuando necesitas varios de estos a la vez.

JunoObtener datosfetch(url) devuelve una promesa para la respuesta del servidor, y cargar datos es dos pasos: await fetch(...) para la respuesta, luego await response.json() para leer los datos de ella. Un error del servidor como "no encontrado" no falla por sí solo, así que verifica response.ok antes de leer el cuerpo. Ambos pasos usan await porque ambos toman tiempo.
JunoObtener datosfetch devuelve una promesa para una Response, y leer el cuerpo con response.json() es un segundo paso async, así que un fetch normal tiene dos awaits. La trampa: fetch solo rechaza en un fallo de red, así que un 404 o 500 aún se resuelve. Verifica response.ok y lanza en un estado malo, y un try/catch cubre todo.
JunoObtener datosfetch se resuelve cuando llegan los encabezados, no el cuerpo, que es por qué response.json() es un paso separado awaited que lee el stream. Solo rechaza en un fallo de red, así que un 404 se resuelve bien y debes verificar response.ok tú mismo. Lanza en un estado malo y un try/catch en el sitio de la llamada maneja fallas de red, estados malos, y fallos de análisis juntos.

El event loop y ejecutar trabajo en paralelo

Todo hasta ahora tiene un tema: JavaScript inicia trabajo lento, sigue adelante, y regresa a él después. Esta sección es el mecanismo detrás de eso, más los patrones para ejecutar varias tareas async bien.

Aquí hay una imagen de cómo JavaScript mantiene un registro del trabajo async. Tu código en ejecución se sienta en la pila de llamadas, la lista de lo que se está ejecutando en este momento. Cuando llamas a setTimeout, el temporizador se entrega al navegador para contar regresivamente, así que no está en la pila bloqueando nada. Cuando el temporizador termina, su callback va a una cola de tareas, una línea de espera de código listo para ejecutarse.

El event loop es la parte que los conecta. Tiene una regla: ejecuta todo en la pila de llamadas primero, y solo cuando la pila está vacía extrae la siguiente tarea de la cola. Por eso setTimeout(fn, 0) aún se ejecuta después de tu otro código, incluso con un retraso de cero. El callback tiene que esperar en la cola hasta que el código actual esté hecho:

js
console.log("primero");
setTimeout(() => console.log("tercero"), 0);
console.log("segundo");
// primero
// segundo
// tercero

El retraso de 0 no significa "ahora mismo". Significa "tan pronto como el código actual termine".

El event loop es el motor que hace que todo este ordenamiento funcione. Tres partes: la pila de llamadas (el código sincrónico ejecutándose ahora mismo), la cola de tareas (callbacks async esperando su turno), y el bucle en sí, que hace una cosa: cuando la pila de llamadas está vacía, toma la siguiente tarea de la cola y la ejecuta.

Esa única regla explica el ordenamiento de setTimeout(fn, 0). El retraso es una espera mínima antes de que el callback se ponga en cola, no una promesa de ejecutarse inmediatamente, y una tarea en cola no puede ejecutarse hasta que el código sincrónico actual termina y vacía la pila:

js
console.log("primero");
setTimeout(() => console.log("tercero"), 0);
console.log("segundo");
// primero
// segundo
// tercero

Cuando tienes varias tareas async independientes, ejecutarlas una tras otra desperdicia tiempo. Esperar en un bucle es el error común:

js
// Lento: cada solicitud espera a que la anterior termine
const users = [];
for (const id of ids) {
  users.push(await loadUser(id));
}

Si las tareas no dependen una de otra, inícialas juntas y espera todas ellas a la vez con Promise.all. Toma un arreglo de promesas y devuelve una promesa que se resuelve a un arreglo de sus resultados:

js
// Rápido: todas las solicitudes se ejecutan al mismo tiempo
const users = await Promise.all(ids.map((id) => loadUser(id)));

Alcanza Promise.all siempre que tengas trabajo async independiente, y mantén el await secuencial en un bucle solo cuando cada paso realmente necesita el resultado del anterior.

El event loop es el planificador que hace posible async de un solo hilo. La pila de llamadas contiene los marcos sincrónico ejecutándose ahora mismo. Los callbacks async no se empujan en ella directamente; esperan en una cola. La regla del bucle es simple: cuando la pila de llamadas está vacía, desencola la siguiente tarea y ejecútala hasta completarse, luego repite. Una tarea se ejecuta sin interrupciones, que es por qué el trabajo sincrónico largo aún bloquea todo, async o no.

Ese modelo es la explicación completa de setTimeout(fn, 0). El retraso programa el callback en la cola después de un tiempo mínimo, pero no puede ejecutarse hasta que la pila se drene, así que siempre sigue al código sincrónico:

js
console.log("primero");
setTimeout(() => console.log("tercero"), 0);
console.log("segundo");
// primero, segundo, tercero

Un refinamiento a mantener en tu cabeza: los callbacks de promesas (los manejadores .then y todo después de un await) van en una cola de microtareas separada y de mayor prioridad que se drena completamente entre tareas, así que la continuación de una promesa resuelta se ejecuta antes de un setTimeout en cola. Raramente necesitas razonar sobre esto, pero explica la sorpresa ocasional donde el código de promesa vence un temporizador.

La recompensa práctica es programar tu propio trabajo bien. Las tareas async independientes deben ejecutarse en paralelo, no en serie. Esperar dentro de un bucle las serializa:

js
// Serial: el tiempo total es la suma de cada solicitud
for (const id of ids) {
  results.push(await loadUser(id));
}

// Paralelo: el tiempo total es aproximadamente la solicitud única más lenta
const results = await Promise.all(ids.map((id) => loadUser(id)));

Promise.all inicia cada promesa inmediatamente (el .map las inicia todas), luego se resuelve una vez que todas se resuelven, rechazando tan pronto como cualquiera rechaza. Úsalo para trabajo independiente; mantén await secuencial solo cuando un paso necesita el resultado anterior. Una precaución sobre condiciones de carrera: si dos operaciones async escriben al mismo estado y no controlas su orden, la última en terminar gana, que puede no ser la última que iniciaste. Cuando el orden importa, await la primera antes de iniciar la segunda, o clave cada resultado para que una llegada tardía no pueda sobrescribir uno más nuevo.

JunoEl event loop Tu código en ejecución se sienta en la pila de llamadas, y cuando un temporizador termina su callback espera en la cola de tareas. El event loop ejecuta todo en la pila primero, luego extrae la siguiente tarea en espera, que es por qué setTimeout(fn, 0) aún se ejecuta después del resto de tu código. Un retraso de 0 significa "tan pronto como el código actual termine", no "ahora mismo".
JunoEl event loop El event loop ejecuta la pila de llamadas hasta vaciarla, luego toma la siguiente tarea de la cola, que es exactamente por qué setTimeout(fn, 0) espera a que tu código sincrónico termine. Cuando las tareas async no dependen una de otra, no las await una por una en un bucle. Inícialas juntas y usa Promise.all, y mantén await secuencial solo cuando un paso necesita el resultado del último.
JunoEl event loop El bucle ejecuta una tarea hasta completarse, luego la siguiente, así que el trabajo sincrónico largo bloquea todo y setTimeout(fn, 0) siempre llega después del código actual. Las continuaciones de promesas se drenan en una cola de microtareas de mayor prioridad entre tareas, que explica el tiempo ocasional en el que un .then vence un temporizador. Ejecuta trabajo independiente con Promise.all en lugar de esperar en un bucle, y cuando dos operaciones tocan el mismo estado, controla el orden o una terminación tardía sobrescribe un resultado más nuevo.

Hacia dónde va async desde aquí

Async es la parte de JavaScript que mantiene una página viva mientras el trabajo lento sucede en segundo plano. La forma es siempre la misma: inicia algo que toma tiempo, deja que el resto de tu código se ejecute, y maneja el resultado cuando llega. Las promesas le dan a ese resultado un valor que puedes pasar, async/await te deja escribirlo en código plano de arriba a abajo, y fetch es donde usarás todo esto más, cargando datos desde un servidor.

Desde aquí, async se conecta con las partes del lenguaje que reaccionan al mundo exterior. Conectarás datos obtenidos en la página a través del DOM, y comenzarás trabajo async en respuesta a lo que la gente hace en la página, que son eventos. Los resultados que obtengas de vuelta son casi siempre objetos, así que leer y reformular ellos es la siguiente habilidad que se empareja con todo aquí.