JavaScript tiene un solo hilo de ejecución. Una cosa a la vez, en orden, sin paralelismo real.
Y sin embargo: hacés un fetch, el resto del código sigue corriendo, y cuando llega la respuesta se ejecuta el callback. Abrís una página con animaciones, timers y eventos de click funcionando todos al mismo tiempo. ¿Cómo?
La respuesta es el event loop.
Las piezas que lo componen
El event loop no es una sola cosa — es la coordinación entre varias:
Call Stack — la pila de ejecución. Cuando llamás una función, se apila. Cuando termina, se desapila. JavaScript ejecuta lo que está en el tope de la pila. Si la pila tiene algo, el hilo está ocupado.
Web APIs — servicios provistos por el browser (o por Node.js) fuera del hilo principal. setTimeout, fetch, eventos del DOM — cuando los llamás, la tarea se delega a estas APIs y el hilo queda libre para seguir.
Task Queue (macrotasks) — cuando una Web API termina su trabajo, pone el callback en esta cola. setTimeout, setInterval, eventos del DOM van acá.
Microtask Queue — una cola de mayor prioridad que la Task Queue. Promises (.then, .catch, async/await) y MutationObserver van acá. Se vacía completamente antes de que el event loop tome el siguiente macrotask.
El event loop en sí hace una sola cosa, en ciclo continuo: si el Call Stack está vacío, toma el siguiente ítem de la cola y lo ejecuta.

El orden que explica todo
console.log('Start')
setTimeout(() => {
console.log('Timeout')
}, 0)
Promise.resolve().then(() => {
console.log('Promise')
})
console.log('End')
Resultado:
Start
End
Promise
Timeout
Por qué:
console.log('Start')— va al stack, ejecuta, se desapilasetTimeout(..., 0)— se delega a la Web API del timer; el callback se encola en la Task Queue cuando termine (inmediatamente, pero aún así después)Promise.resolve().then(...)— el callback se encola en la Microtask Queueconsole.log('End')— va al stack, ejecuta, se desapila- Stack vacío → el event loop vacía la Microtask Queue primero → ejecuta
'Promise' - Stack vacío, Microtask Queue vacía → toma el siguiente macrotask → ejecuta
'Timeout'
El setTimeout(..., 0) no significa "ejecutá esto ahora". Significa "ejecutá esto lo antes posible después de que el stack esté vacío y las microtasks estén resueltas". La diferencia importa.
Microtasks vs macrotasks
La distinción entre las dos colas no es arbitraria. Las microtasks tienen prioridad porque están diseñadas para reacciones inmediatas al estado actual — el .then de una Promise debe ejecutarse antes de que el browser pinte la pantalla o procese el siguiente evento.
Promise.resolve()
.then(() => {
console.log('microtask 1')
return Promise.resolve()
})
.then(() => console.log('microtask 2'))
setTimeout(() => console.log('macrotask'), 0)
// microtask 1
// microtask 2
// macrotask
Todas las microtasks encadenadas se ejecutan antes de que el event loop procese el próximo macrotask. Si una microtask encola otra microtask, esa también se ejecuta antes de los macrotasks.
Por qué el stack bloqueado congela todo
Si una operación sincrónica tarda mucho — un loop que procesa un array de un millón de elementos, una función de encriptación pesada — bloquea el call stack. Mientras el stack tiene algo, el event loop no puede procesar nada de las colas.
// Esto bloquea el hilo por el tiempo que tarde
function procesarMillon() {
for (let i = 0; i < 1_000_000; i++) {
// cálculo pesado
}
}
procesarMillon() // UI congelada, eventos no responden, timers no disparan
La UI deja de responder. Los clicks no se procesan. Los timers se atrasan. Todo espera a que el stack se vacíe.
Es el motivo por el que las operaciones CPU-intensivas van en Web Workers — para correrlas en un hilo separado sin bloquear el event loop.
Cómo debuggear problemas de timing
Chrome DevTools → Sources → Call Stack muestra qué hay en la pila en cualquier breakpoint. Si ves callbacks apilados de forma inesperada, ahí está el problema.
Performance Profiler registra una línea de tiempo de ejecución. Los bloques largos en el hilo principal (long tasks) son tareas que bloquearon el event loop por más de 50ms — la métrica que usa Chrome para marcar jank.
console.log estratégico sigue siendo la forma más rápida de confirmar el orden de ejecución cuando algo no ocurre cuando esperás.
Lo que el event loop no resuelve
El event loop hace posible la concurrencia asincrónica — múltiples operaciones de I/O en vuelo al mismo tiempo. No hace paralelismo real.
Si necesitás correr código en paralelo para aprovechar múltiples cores, necesitás Workers. El event loop maneja "muchas cosas esperando"; los Workers manejan "muchas cosas corriendo".
Entender la diferencia entre los dos modelos es lo que separa saber usar async/await de entender por qué funciona.