Asincronía

Por qué JavaScript necesita la asincronía, y cómo dominarla con callbacks, promesas y async/await.

JavaScript se ejecuta en un solo hilo. Si una operación tardara (como leer un archivo o pedir datos a un servidor), el código se bloquearía y la página quedaría congelada. La asincronía es la solución: encuestar el resultado más tarde, sin detener el resto.


El problema con el código síncrono

// Si fetch() fuera síncrono, esto bloquearía el navegador durante segundos
const datos = fetch('https://api.ejemplo.com/datos');  // imaginemos
mostrarSpinner();  // ← nunca llegaría aquí hasta que fetch terminara

JavaScript resuelve esto con un event loop: cuando una operación asíncrona termina, pone el callback en una cola, y el event loop lo ejecuta cuando el hilo principal esté libre.


Callbacks

La forma original de asincronía. Pasas una función que se llamará cuando la operación termine:

setTimeout(() => {
  console.log('ejecutado 2 segundos después');
}, 2000);

console.log('esto se ejecuta primero');

El problema aparece cuando encadenas operaciones asíncronas. Se crea el infame callback hell:

// ❌ Callback hell — ilegible, difícil de depurar
obtenerUsuario(1, (usuario) => {
  obtenerPedidos(usuario.id, (pedidos) => {
    obtenerDetalle(pedidos[0].id, (detalle) => {
      mostrar(detalle);
    }, (error) => { console.error(error) });
  }, (error) => { console.error(error) });
}, (error) => { console.error(error) });

Promesas

Una promesa (Promise) representa un valor que estará disponible en el futuro. Tiene tres estados: pendiente, resuelta o rechazada.

const promesa = new Promise((resolve, reject) => {
  setTimeout(() => {
    const exito = true;
    if (exito) {
      resolve('¡Datos recibidos!');  // la promesa se resuelve
    } else {
      reject(new Error('Algo salió mal'));  // la promesa se rechaza
    }
  }, 1000);
});

promesa
  .then((resultado) => console.log(resultado))  // '¡Datos recibidos!'
  .catch((error) => console.error(error.message))
  .finally(() => console.log('siempre se ejecuta'));

Las promesas permiten encadenar con .then(), lo que aplana el código:

// ✅ Promesas encadenadas — mucho más legible
obtenerUsuario(1)
  .then((usuario) => obtenerPedidos(usuario.id))
  .then((pedidos) => obtenerDetalle(pedidos[0].id))
  .then((detalle) => mostrar(detalle))
  .catch((error) => console.error(error));  // un solo catch para toda la cadena

async / await

La sintaxis async/await es azúcar sintáctico sobre las promesas. Hace que el código asíncrono se lea como si fuera síncrono:

async function cargarDatos() {
  // await pausa la función hasta que la promesa se resuelva
  // el resto del programa sigue ejecutándose
  const usuario = await obtenerUsuario(1);
  const pedidos = await obtenerPedidos(usuario.id);
  const detalle = await obtenerDetalle(pedidos[0].id);
  mostrar(detalle);
}

El manejo de errores con try/catch:

async function cargarPerfil(id) {
  try {
    const res = await fetch(`/api/usuarios/${id}`);

    if (!res.ok) {
      throw new Error(`HTTP ${res.status}`);
    }

    const usuario = await res.json();
    return usuario;

  } catch (error) {
    console.error('Error cargando perfil:', error.message);
    return null;  // valor por defecto
  }
}

Importante: await solo puede usarse dentro de una función async. En el nivel superior de un módulo ES (<script type="module">), también se puede usar directamente.


Errores frecuentes con async/await

// ❌ Olvidar el await — promesa sin resolver, siempre truthy
async function obtenerNombre() {
  const usuario = fetch('/api/usuario');  // falta await
  if (usuario) { ... }                   // siempre true
}

// ❌ await en forEach — no espera
const ids = [1, 2, 3];
ids.forEach(async (id) => {
  const res = await fetch(`/api/${id}`);  // forEach ignora las promesas
  console.log(await res.json());
});

// ✅ for...of para awaits en bucles
for (const id of ids) {
  const res = await fetch(`/api/${id}`);
  console.log(await res.json());
}

Operaciones en paralelo

Cuando las operaciones son independientes entre sí, puedes lanzarlas en paralelo:

// ❌ Secuencial: espera una, luego otra (2s + 2s = 4s)
const usuario = await fetch('/api/usuario');
const config = await fetch('/api/config');

// ✅ Paralelo: ambas se lanzan a la vez (max 2s)
const [usuario, config] = await Promise.all([
  fetch('/api/usuario'),
  fetch('/api/config')
]);

Métodos de Promise

// Promise.all — espera todas, falla si una falla
const resultados = await Promise.all([p1, p2, p3]);

// Promise.allSettled — espera todas, aunque alguna falle
const todos = await Promise.allSettled([p1, p2, p3]);
todos.forEach((r) => {
  if (r.status === 'fulfilled') console.log(r.value);
  if (r.status === 'rejected') console.error(r.reason);
});

// Promise.race — devuelve la primera que se resuelva o rechace
const primera = await Promise.race([p1, p2, p3]);

// Promise.any — devuelve la primera que se resuelva (ignora rechazos)
const primerExito = await Promise.any([p1, p2, p3]);

El event loop (resumen visual)

Código síncrono

  Call Stack (se ejecuta)

 Tarea asíncrona → Web API (setTimeout, fetch...) → esperar

  Callback Queue (cuando termina)

  Event Loop: ¿Call Stack vacío? → mueve callback al stack

  Se ejecuta el callback

Las promesas y async/await usan la microtask queue, que tiene prioridad sobre la callback queue. Por eso los .then() se ejecutan antes que los setTimeout.


En la siguiente lección ponemos la asincronía en práctica aprendiendo a usar la Fetch API para comunicarse con servidores y APIs REST.