Skip to content

Asincronía ​

Pedir datos a una API, esperar a que el usuario termine de escribir, subir una imagen, mostrar una notificación en 3 segundos... Muchas tareas tardan, y JavaScript no puede quedarse congelado esperándolas. En esta lección entenderás cómo JavaScript hace varias cosas "a la vez" con callbacks, promesas y async/await, y qué es el famoso event loop.

Concepto ​

Síncrono vs. asíncrono ​

Imagina una cafetería:

  • Síncrona: el cajero toma tu orden, prepara el café, te lo entrega y sólo entonces atiende al siguiente cliente. Si un café tarda 5 minutos, la fila entera espera.
  • Asíncrona: el cajero toma tu orden, te da un ticket (una promesa) y atiende al siguiente. Cuando tu café está listo, te llaman por tu nombre.

JavaScript tiene un solo hilo (un solo cajero). Si hiciera las tareas lentas de forma síncrona, la página se congelaría: no podrías hacer clic ni hacer scroll. Por eso las operaciones lentas (red, temporizadores, lectura de archivos) se delegan al navegador y JavaScript sigue con lo demás.

js
console.log('1. Tomo la orden');

setTimeout(() => {
  console.log('3. ¡Café listo!');
}, 2000);

console.log('2. Atiendo al siguiente cliente');

// Salida:
// 1. Tomo la orden
// 2. Atiendo al siguiente cliente
// 3. ¡Café listo!   (2 segundos después)

Temporizadores ​

js
// Ejecutar una vez después de X milisegundos
const id = setTimeout(() => console.log('Hola'), 1000);
clearTimeout(id); // cancelarlo antes de que ocurra

// Ejecutar cada X milisegundos
let segundos = 0;
const intervalo = setInterval(() => {
  segundos++;
  console.log(`Han pasado ${segundos} s`);
  if (segundos === 5) clearInterval(intervalo); // detenerlo
}, 1000);

Error común

setTimeout(fn, 0) no se ejecuta inmediatamente: se ejecuta cuando JavaScript termine todo el código síncrono pendiente. Y el tiempo es un mínimo, no una garantía exacta.

Callbacks ​

La forma original de manejar la asincronía: pasar una función que se ejecutará cuando la tarea termine.

js
function obtenerUsuario(id, callback) {
  setTimeout(() => {
    // simulamos una consulta a una base de datos
    if (id <= 0) {
      callback(new Error('ID inválido'), null);
      return;
    }
    callback(null, { id, nombre: 'Ana' });
  }, 1000);
}

obtenerUsuario(1, (error, usuario) => {
  if (error) {
    console.error(error.message);
    return;
  }
  console.log('Usuario:', usuario.nombre);
});

Callback hell ​

El problema aparece cuando una tarea depende de otra, que depende de otra...

js
obtenerUsuario(1, (err, usuario) => {
  if (err) return manejar(err);
  obtenerPedidos(usuario.id, (err, pedidos) => {
    if (err) return manejar(err);
    obtenerDetalle(pedidos[0].id, (err, detalle) => {
      if (err) return manejar(err);
      calcularEnvio(detalle, (err, envio) => {
        if (err) return manejar(err);
        console.log('Envío:', envio);   // 😵 la "pirámide de la perdición"
      });
    });
  });
});

Difícil de leer, de manejar errores y de mantener. Las promesas nacieron para resolver esto.

Promesas ​

Una promesa es un objeto que representa un valor que estará disponible en el futuro. Es el ticket de la cafetería. Tiene tres estados:

  • pending (pendiente): aún esperando.
  • fulfilled (cumplida): terminó bien y tiene un valor.
  • rejected (rechazada): falló y tiene un error.

Una vez cumplida o rechazada, ya no cambia.

Consumir una promesa: then, catch, finally ​

js
fetch('https://jsonplaceholder.typicode.com/users/1') // fetch devuelve una promesa
  .then((respuesta) => respuesta.json())   // devuelve otra promesa
  .then((usuario) => {
    console.log(usuario.name);
  })
  .catch((error) => {
    console.error('Algo falló:', error);    // captura errores de CUALQUIER paso
  })
  .finally(() => {
    console.log('Terminó (bien o mal)');    // ocultar el "cargando..."
  });

Cada .then devuelve una nueva promesa, por eso se pueden encadenar en lugar de anidar.

Crear una promesa ​

js
function esperar(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

esperar(1500).then(() => console.log('Pasaron 1.5 s'));

// Convertir el callback de antes en promesa
function obtenerUsuario(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id <= 0) reject(new Error('ID inválido'));
      else resolve({ id, nombre: 'Ana' });
    }, 1000);
  });
}

obtenerUsuario(1)
  .then((usuario) => console.log(usuario))
  .catch((error) => console.error(error.message));

El callback hell, reescrito con promesas:

js
obtenerUsuario(1)
  .then((usuario) => obtenerPedidos(usuario.id))
  .then((pedidos) => obtenerDetalle(pedidos[0].id))
  .then((detalle) => calcularEnvio(detalle))
  .then((envio) => console.log('Envío:', envio))
  .catch(manejar);   // un solo lugar para los errores

async / await ​

async/await es azúcar sintáctica sobre las promesas: te permite escribir código asíncrono que se lee como síncrono.

  • async antes de una función hace que siempre devuelva una promesa.
  • await pausa la función (sólo esa función, no la página) hasta que la promesa se resuelva, y te da su valor.
js
async function mostrarEnvio() {
  try {
    const usuario = await obtenerUsuario(1);
    const pedidos = await obtenerPedidos(usuario.id);
    const detalle = await obtenerDetalle(pedidos[0].id);
    const envio = await calcularEnvio(detalle);
    console.log('Envío:', envio);
  } catch (error) {
    console.error('Error:', error.message);
  } finally {
    ocultarCargando();
  }
}

mostrarEnvio();

También funciona con funciones flecha y métodos:

js
const cargarDatos = async () => { /* ... */ };

const api = {
  async obtener(url) { /* ... */ }
};

Top-level await

Dentro de módulos ES (<script type="module">) puedes usar await directamente, sin función async:

js
const respuesta = await fetch('/api/config');

Error común: olvidar el await

js
async function cargar() {
  const usuario = obtenerUsuario(1);   // falta await
  console.log(usuario.nombre);         // undefined: usuario es una Promise
}

Si ves Promise { <pending> } en la consola, te faltó un await o un .then.

Ejecutar promesas en paralelo ​

Si las tareas no dependen entre sí, no las esperes una por una: lánzalas al mismo tiempo.

js
// ❌ Secuencial: tarda 1 s + 1 s + 1 s = 3 s
const usuarios = await obtenerUsuarios();
const productos = await obtenerProductos();
const categorias = await obtenerCategorias();

// ✅ En paralelo: tarda ~1 s (lo que tarde la más lenta)
const [usuarios2, productos2, categorias2] = await Promise.all([
  obtenerUsuarios(),
  obtenerProductos(),
  obtenerCategorias()
]);

Los combinadores de promesas:

js
// Promise.all: espera a TODAS. Si UNA falla, falla todo.
await Promise.all([p1, p2, p3]);

// Promise.allSettled: espera a todas y te dice cuál salió bien y cuál mal.
const resultados = await Promise.allSettled([p1, p2, p3]);
resultados.forEach((r) => {
  if (r.status === 'fulfilled') console.log('OK', r.value);
  else console.log('Falló', r.reason);
});

// Promise.race: la PRIMERA que termine (bien o mal) gana. Útil para timeouts.
const conTimeout = (promesa, ms) =>
  Promise.race([
    promesa,
    new Promise((_, reject) => setTimeout(() => reject(new Error('Tiempo agotado')), ms))
  ]);

// Promise.any: la primera que se CUMPLA. Falla sólo si todas fallan.
const masRapido = await Promise.any([
  fetch('https://servidor-1.com/datos'),
  fetch('https://servidor-2.com/datos')
]);

Buena práctica

Usa Promise.allSettled en dashboards: si el widget de ventas falla, los demás widgets igual deben mostrarse.

El event loop ​

¿Cómo hace JavaScript, con un solo hilo, para manejar tantas tareas? Gracias al event loop (bucle de eventos). Sus piezas:

  1. Call stack (pila de llamadas): donde se ejecuta el código, una función a la vez.
  2. Web APIs: el navegador se encarga de las tareas lentas (temporizadores, fetch, eventos).
  3. Microtask queue (cola de microtareas): callbacks de promesas (.then, código después de await), queueMicrotask.
  4. Task queue (cola de tareas o macrotareas): callbacks de setTimeout, setInterval, eventos del DOM.

El event loop repite este ciclo:

txt
┌──────────────────────────────────────────────┐
│ 1. Ejecuta TODO el código síncrono (stack)   │
│ 2. ¿Stack vacío? Ejecuta TODAS las microtareas│
│ 3. El navegador puede repintar la pantalla   │
│ 4. Toma UNA tarea de la task queue           │
│ 5. Vuelve al paso 2                           │
└──────────────────────────────────────────────┘

Las microtareas (promesas) siempre van antes que las tareas (setTimeout). Pon a prueba tu intuición:

js
console.log('A');

setTimeout(() => console.log('B'), 0);

Promise.resolve().then(() => console.log('C'));

queueMicrotask(() => console.log('D'));

(async () => {
  console.log('E');
  await null;
  console.log('F');
})();

console.log('G');
¿Cuál es el orden? (piénsalo antes de abrir)
txt
A  ← síncrono
E  ← síncrono (una función async se ejecuta síncronamente hasta el primer await)
G  ← síncrono
C  ← microtarea
D  ← microtarea
F  ← microtarea (lo que sigue al await)
B  ← tarea (setTimeout), al final aunque tenga 0 ms

Experimenta con el orden de ejecución en este demo:

Event loop: orden de ejecución

Error común: bloquear el hilo

Un bucle pesado (procesar 10 millones de elementos) bloquea el stack: la página se congela y no responde a clics. Para cálculos muy pesados usa Web Workers, que corren en otro hilo.

Casos de uso reales ​

js
// 1. Reintentar una operación que falla (por ejemplo, red inestable)
async function conReintentos(fn, intentos = 3, espera = 1000) {
  for (let i = 1; i <= intentos; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === intentos) throw error;
      console.warn(`Intento ${i} falló, reintentando...`);
      await esperar(espera * i); // espera cada vez más (backoff)
    }
  }
}

const datos = await conReintentos(() => fetch('/api/datos').then((r) => r.json()));

// 2. Procesar en lotes para no saturar un servidor
async function procesarEnLotes(items, tamanoLote, fn) {
  const resultados = [];
  for (let i = 0; i < items.length; i += tamanoLote) {
    const lote = items.slice(i, i + tamanoLote);
    resultados.push(...(await Promise.all(lote.map(fn))));
  }
  return resultados;
}

// 3. Notificación que desaparece sola
async function mostrarToast(mensaje) {
  const toast = document.createElement('div');
  toast.className = 'toast';
  toast.textContent = mensaje;
  document.body.append(toast);
  await esperar(3000);
  toast.remove();
}

Ejercicios ​

  1. Escribe esperar(ms) y úsala con async/await para mostrar "3", "2", "1", "¡Despegue!" con un segundo entre cada uno.
  2. Convierte esta función de callback a promesa: function leer(cb) { setTimeout(() => cb(null, 'datos'), 500); }.
  3. Tienes obtenerPrecio(producto) que devuelve una promesa. Obtén en paralelo los precios de ['mouse', 'teclado', 'monitor'] y suma el total.
  4. Escribe conTimeout(promesa, ms) que rechace con "Tiempo agotado" si la promesa tarda más de ms.
  5. Predice el orden: setTimeout(() => console.log(1)); Promise.resolve().then(() => console.log(2)); console.log(3);
Ver solución
js
// 1
const esperar = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function cuentaRegresiva() {
  for (const n of [3, 2, 1]) {
    console.log(n);
    await esperar(1000);
  }
  console.log('¡Despegue!');
}

// 2
function leer() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('datos'), 500);
  });
}

// 3
async function totalPrecios() {
  const productos = ['mouse', 'teclado', 'monitor'];
  const precios = await Promise.all(productos.map(obtenerPrecio));
  return precios.reduce((a, b) => a + b, 0);
}

// 4
function conTimeout(promesa, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Tiempo agotado')), ms)
  );
  return Promise.race([promesa, timeout]);
}

// 5 → 3, 2, 1  (síncrono, microtarea, tarea)

Resumen ​

  • JavaScript tiene un solo hilo; las tareas lentas se delegan y su resultado llega después.
  • Los callbacks funcionan, pero anidados generan "callback hell".
  • Una promesa representa un valor futuro: pending, fulfilled o rejected. Se consume con .then/.catch/.finally.
  • async/await permite escribir código asíncrono legible; maneja errores con try/catch.
  • Usa Promise.all para tareas independientes en paralelo, allSettled cuando algunas pueden fallar y race para timeouts.
  • El event loop ejecuta primero el código síncrono, luego todas las microtareas (promesas) y después una tarea (setTimeout).

Siguiente: Fetch y APIs REST

Academia Web — aprende, copia y construye.