El Event Loop: cómo JavaScript nunca se bloquea
JavaScript ejecuta código en un solo hilo. El Event Loop es el mecanismo que le permite manejar operaciones asíncronas sin bloquearse — es la razón por la que puedes hacer click en un botón mientras se carga una imagen.
JavaScript es single-threaded
Un solo hilo significa que JavaScript ejecuta una sola instrucción a la vez. No hay paralelismo real — pero hay concurrencia gracias al Event Loop:
// Todo esto se ejecuta en el mismo hilo, en secuencia
console.log("1 — síncrono");
setTimeout(() => {
console.log("3 — macrotask");
}, 0);
Promise.resolve().then(() => {
console.log("2 — microtask");
});
console.log("1b — también síncrono");
// Orden de salida:
// 1 — síncrono
// 1b — también síncrono
// 2 — microtask
// 3 — macrotask// TypeScript compila a JavaScript — el runtime es idéntico
console.log("1 — síncrono");
setTimeout((): void => {
console.log("3 — macrotask");
}, 0);
Promise.resolve().then((): void => {
console.log("2 — microtask");
});
console.log("1b — también síncrono");
// Orden de salida:
// 1 — síncrono
// 1b — también síncrono
// 2 — microtask
// 3 — macrotask1 — síncrono
1b — también síncrono
2 — microtask
3 — macrotask- Código síncrono (Call Stack) — hasta que el stack quede vacío
- Microtask Queue — todas las microtasks pendientes (Promises, queueMicrotask)
- Macrotask Queue — una sola macrotask (setTimeout, setInterval, I/O)
- Volver al paso 2 si hay microtasks nuevas
El Call Stack — LIFO
El Call Stack registra qué función está ejecutando el motor. Funciona como una pila: la última en entrar es la primera en salir (LIFO):
// Observa cómo el stack crece y decrece
function sumar(a, b) {
return a + b; // [sumar] ← tope del stack
}
function calcular(x) {
const resultado = sumar(x, 10); // [calcular, sumar]
return resultado;
}
function iniciar() {
const valor = calcular(5); // [iniciar, calcular]
console.log(valor); // [iniciar] → imprime 15
}
iniciar(); // [iniciar] entra al stack
// Stack en cada momento:
// → [iniciar]
// → [iniciar, calcular]
// → [iniciar, calcular, sumar]
// → [iniciar, calcular] (sumar retorna 15)
// → [iniciar] (calcular retorna 15)
// → [] (iniciar termina)
// Un stack overflow ocurre si las llamadas no terminan
function infinita() {
return infinita(); // Maximum call stack size exceeded
}function sumar(a: number, b: number): number {
return a + b;
}
function calcular(x: number): number {
const resultado = sumar(x, 10);
return resultado;
}
function iniciar(): void {
const valor = calcular(5);
console.log(valor); // 15
}
iniciar();
// Stack en cada momento:
// → [iniciar]
// → [iniciar, calcular]
// → [iniciar, calcular, sumar]
// → [iniciar, calcular]
// → [iniciar]
// → []15Las Web APIs — donde vive el código async
Cuando el motor encuentra setTimeout, fetch, o un event listener, delega la operación a las Web APIs (browser) o a las libuv APIs (Node.js) — liberando el Call Stack:
console.log("A — inicio");
// 1. fetch() sale del stack y va a las Web APIs
// El stack queda libre para seguir ejecutando
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then(respuesta => respuesta.json())
.then(datos => {
// Este código no bloquea — llega aquí cuando la red responde
console.log("C — datos:", datos.title);
});
// 2. setTimeout también va a las Web APIs
setTimeout(() => {
console.log("D — timeout completado");
}, 100);
// 3. Este código se ejecuta INMEDIATAMENTE mientras fetch/setTimeout esperan
console.log("B — código síncrono continúa");
// Orden:
// A — inicio
// B — código síncrono continúa
// C — datos: ... (cuando la red responde)
// D — timeout completado (después de ~100ms)type Todo = { id: number; title: string; completed: boolean };
console.log("A — inicio");
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((respuesta: Response) => respuesta.json())
.then((datos: Todo) => {
console.log("C — datos:", datos.title);
});
setTimeout((): void => {
console.log("D — timeout completado");
}, 100);
console.log("B — código síncrono continúa");
// Orden:
// A — inicio
// B — código síncrono continúa
// C — datos: ... (cuando la red responde)
// D — timeout completadoLa Macrotask Queue — tareas del sistema
Cuando un setTimeout, setInterval, o evento de I/O completa, su callback entra a la Macrotask Queue. El Event Loop lo recoge cuando el stack está vacío y las microtasks procesadas:
// Los macrotasks se ejecutan uno por uno, con un "tick" entre cada uno
setTimeout(() => console.log("Macrotask 1"), 0);
setTimeout(() => console.log("Macrotask 2"), 0);
setTimeout(() => console.log("Macrotask 3"), 0);
// El Event Loop procesa microtasks entre cada macrotask
setTimeout(() => {
console.log("Macrotask A");
Promise.resolve().then(() => {
console.log(" Microtask dentro de Macrotask A");
});
}, 0);
setTimeout(() => {
console.log("Macrotask B");
}, 0);
// Salida:
// Macrotask 1
// Macrotask 2
// Macrotask 3
// Macrotask A
// Microtask dentro de Macrotask A ← procesada ANTES de la siguiente macrotask
// Macrotask BsetTimeout((): void => console.log("Macrotask 1"), 0);
setTimeout((): void => console.log("Macrotask 2"), 0);
setTimeout((): void => console.log("Macrotask 3"), 0);
setTimeout((): void => {
console.log("Macrotask A");
Promise.resolve().then((): void => {
console.log(" Microtask dentro de Macrotask A");
});
}, 0);
setTimeout((): void => {
console.log("Macrotask B");
}, 0);
// Salida:
// Macrotask 1
// Macrotask 2
// Macrotask 3
// Macrotask A
// Microtask dentro de Macrotask A
// Macrotask BMacrotask 1
Macrotask 2
Macrotask 3
Macrotask A
Microtask dentro de Macrotask A
Macrotask BLa Microtask Queue — prioridad máxima
Las microtasks (Promises, queueMicrotask) tienen prioridad sobre las macrotasks. El motor vacía todas las microtasks antes de procesar la siguiente macrotask:
// queueMicrotask — forma explícita de agregar microtasks
console.log("1 — sync");
queueMicrotask(() => console.log("3 — microtask (queueMicrotask)"));
Promise.resolve()
.then(() => {
console.log("4 — microtask (Promise)");
// Agregar microtask desde microtask — se procesa antes del próximo macrotask
queueMicrotask(() => console.log("5 — microtask anidada"));
});
setTimeout(() => console.log("6 — macrotask"), 0);
console.log("2 — sync");
// 1 — sync
// 2 — sync
// 3 — microtask (queueMicrotask)
// 4 — microtask (Promise)
// 5 — microtask anidada ← se inserta y procesa antes del macrotask
// 6 — macrotaskconsole.log("1 — sync");
queueMicrotask((): void => console.log("3 — microtask (queueMicrotask)"));
Promise.resolve()
.then((): void => {
console.log("4 — microtask (Promise)");
queueMicrotask((): void => console.log("5 — microtask anidada"));
});
setTimeout((): void => console.log("6 — macrotask"), 0);
console.log("2 — sync");
// 1 — sync
// 2 — sync
// 3 — microtask (queueMicrotask)
// 4 — microtask (Promise)
// 5 — microtask anidada
// 6 — macrotask1 — sync
2 — sync
3 — microtask (queueMicrotask)
4 — microtask (Promise)
5 — microtask anidada
6 — macrotaskSi una microtask agrega otra microtask que agrega otra, y así sucesivamente, el motor nunca llega a procesar macrotasks ni a repintar el browser. Usa queueMicrotask con cuidado — para diferir trabajo pequeño, no para reemplazar setTimeout.