Command Palette

Search for a command to run...

Event Loop y Call Stack: Cómo JavaScript Ejecuta Código Asíncrono

Descubre el mecanismo interno que permite a JavaScript manejar operaciones asíncronas sin bloquear el hilo principal, entendiendo el Event Loop y la Call Stack.

Lectura: 15 min
Nivel: Intermedio

TL;DR - Resumen rápido

  • JavaScript es single-threaded: solo puede ejecutar una tarea a la vez
  • La Call Stack es una estructura LIFO que rastrea las funciones en ejecución
  • Las operaciones asíncronas se delegan a Web APIs (setTimeout, fetch, etc.)
  • El Event Loop coordina la Call Stack con la Task Queue
  • JavaScript ejecuta todo el código síncrono antes de procesar tareas asíncronas

Introducción al Event Loop

El Event Loop es el mecanismo que permite a JavaScript, siendo un lenguaje single-threaded, ejecutar operaciones asíncronas sin bloquear el hilo principal. Este componente coordina la ejecución de código entre la Call Stack, las Web APIs, y diferentes colas de tareas (Task Queue y Microtask Queue), determinando el orden en que se ejecuta tu código.

Entender el Event Loop es fundamental para predecir el comportamiento de operaciones asíncronas como setTimeout, fetch o Promise. Sin este conocimiento, es difícil depurar código asíncrono complejo o entender por qué cierto código no se ejecuta en el orden que esperas.

¿Por qué es importante?

Entender el Event Loop te ayudará a predecir el orden de ejecución de tu código, identificar cuellos de botella de rendimiento y escribir código asíncrono más eficiente. Es la base para dominar promesas, async/await y cualquier operación asíncrona en JavaScript.

La Call Stack

La Call Stack (pila de llamadas) es una estructura de datos LIFO (Last In, First Out) que JavaScript utiliza para rastrear qué función se está ejecutando actualmente y qué funciones se deben ejecutar después. Cada vez que se invoca una función, se añade a la parte superior de la pila. Cuando la función termina, se elimina de la pila.

Ejecución Síncrona en la Call Stack

En código síncrono, las funciones se ejecutan en el orden exacto en que se llaman. JavaScript procesa cada función completamente antes de pasar a la siguiente, lo que garantiza un flujo de ejecución predecible y determinista.

call-stack-sincrona.js
Loading code...

Este ejemplo muestra cómo JavaScript procesa funciones síncronas. Cada función se añade a la Call Stack, se ejecuta completamente, y luego se elimina antes de que la siguiente función comience. Este comportamiento es predecible y fácil de entender, pero tiene una limitación crítica: si una función tarda mucho en ejecutarse, bloquea todo el hilo principal, haciendo que la interfaz de usuario no responda.

Advertencia de Bloqueo

Las operaciones síncronas pesadas (como cálculos complejos o bucles largos) pueden bloquear el hilo principal, haciendo que tu aplicación deje de responder. Esto es especialmente crítico en aplicaciones web donde la experiencia del usuario depende de una interfaz fluida.

Modelo Single-Threaded de JavaScript

JavaScript es un lenguaje single-threaded, lo que significa que solo puede ejecutar una tarea a la vez. Esto puede parecer una limitación, pero en realidad simplifica el desarrollo: no tienes que preocuparte por condiciones de carrera, deadlocks o problemas de concurrencia complejos que existen en lenguajes multi-threaded.

Sin embargo, esta característica plantea un desafío: ¿cómo maneja JavaScript operaciones que toman tiempo, como hacer peticiones de red, leer archivos o animaciones? La respuesta está en el modelo de concurrencia de JavaScript, que incluye Web APIs, Task Queue y el Event Loop.

Operaciones Asíncronas y Web APIs

Las operaciones asíncronas como setTimeout, fetch,addEventListener o Promise no se ejecutan directamente en el hilo principal de JavaScript. En su lugar, son delegadas a las Web APIs proporcionadas por el navegador (o Node.js en el caso del servidor). Estas APIs manejan las operaciones en segundo plano y notifican a JavaScript cuando están completas.

web-apis-asincronas.js
Loading code...

En este ejemplo, setTimeout no bloquea la ejecución del código. La función de callback se coloca en la Task Queue después de que el tiempo especificado ha pasado, pero no se ejecuta hasta que la Call Stack esté vacía. Esto es fundamental para entender el orden de ejecución en JavaScript asíncrono.

  • <strong>setTimeout/setInterval</strong>: Macrotasks que se ejecutan después de un tiempo especificado
  • <strong>fetch/XMLHttpRequest</strong>: Peticiones HTTP que retornan promesas (microtasks)
  • <strong>addEventListener</strong>: Eventos del DOM que van a la Task Queue
  • <strong>Promise</strong>: Microtasks que tienen mayor prioridad que setTimeout
  • <strong>requestAnimationFrame</strong>: Cola especial para animaciones (60fps)

queueMicrotask: Control explícito

La API queueMicrotask(callback) te permite agregar tareas directamente a la Microtask Queue sin crear una promesa. Es útil cuando necesitas garantizar que tu código se ejecute antes que cualquier macrotask pero después del código síncrono actual, y es más eficiente que Promise.resolve().then(callback).

El Event Loop

El Event Loop es el componente que coordina la ejecución de código JavaScript. Su trabajo es monitorear tanto la Call Stack como la Task Queue. Cuando la Call Stack está vacía, el Event Loop toma la primera tarea de la Task Queue y la empuja a la Call Stack para ser ejecutada. Este proceso se repite continuamente, creando un "loop" infinito.

Este mecanismo permite que JavaScript maneje operaciones asíncronas sin bloquear el hilo principal. Mientras el código síncrono se ejecuta, las operaciones asíncronas pueden ocurrir en segundo plano a través de las Web APIs. Cuando estas operaciones terminan, sus callbacks se colocan en la Task Queue, esperando su turno para ejecutarse.

¿Cómo Funciona el Event Loop?

El Event Loop sigue un ciclo continuo con fases específicas: primero ejecuta todo el código síncrono en la Call Stack hasta que esté vacía. Segundo, procesa completamente la Microtask Queue (promesas resueltas, queueMicrotask, MutationObserver). Tercero, toma una macrotask de la Task Queue (setTimeout, setInterval, I/O callbacks). Este ciclo se repite infinitamente. La clave es que TODAS las microtasks se procesan antes de pasar a la siguiente macrotask, lo que explica la prioridad de las promesas.

event-loop-ciclo.js
Loading code...

Este ejemplo demuestra la prioridad de ejecución del Event Loop. El setTimeoutcon delay de 0ms no se ejecuta inmediatamente después del código síncrono, sino que debe esperar a que se vacíe la Microtask Queue. Las promesas tienen prioridad absoluta porque el Event Loop vacía completamente la Microtask Queue antes de procesar la siguiente macrotask. Esta es la razón por la que las promesas siempre se ejecutan antes que setTimeout, independientemente del delay.

Prioridad de Ejecución

El Event Loop siempre procesa primero la Microtask Queue (promesas) antes de la Task Queue (setTimeout, setInterval). Esto significa que las promesas resueltas siempre se ejecutarán antes que los callbacks de setTimeout con el mismo delay.

Orden de Ejecución Completo

Para predecir correctamente el orden de ejecución de tu código, debes entender cómo interactúan todos los componentes: Call Stack, Web APIs, Microtask Queue, Task Queue y Event Loop. El siguiente ejemplo muestra un escenario complejo que combina todos estos elementos.

orden-ejecucion-completo.js
Loading code...

Este ejemplo muestra interacciones complejas entre colas: cuando una macrotask crea una microtask (Promise dentro de setTimeout), esa microtask se procesa inmediatamente después de que la macrotask actual termine, antes de pasar a la siguiente macrotask. Por eso "Promise dentro de setTimeout" se ejecuta antes que "setTimeout dentro de Promise", aunque esta última fue programada primero. Este comportamiento es crítico para entender el timing en código asíncrono complejo.

Errores Comunes con el Event Loop

Los siguientes errores son comunes incluso entre desarrolladores experimentados. Estos problemas suelen manifestarse como bugs de timing difíciles de reproducir o comportamientos inesperados en producción.

Error 1: Bloqueo del Hilo Principal

Asumir que setTimeout(fn, 0) ejecuta el código "en paralelo" o permite que otras operaciones se ejecuten mientras tanto es incorrecto. El setTimeoutsolo programa el callback para el siguiente ciclo del Event Loop, no ejecuta código en un hilo separado. El código síncrono siempre bloquea el hilo principal hasta que termina.

error-bloqueo-hilo.js
Loading code...

El bucle síncrono bloquea completamente el hilo principal durante varios segundos. Durante este tiempo, el navegador no puede renderizar, responder a eventos del usuario, ni ejecutar ningún código JavaScript. El callback de setTimeout permanece en la Task Queue esperando hasta que la Call Stack se vacíe. Para operaciones pesadas, la solución es usar Web Workers (código en hilo separado) o dividir el trabajo en chunks pequeños usando setTimeout recursivo.

Error 2: Confundir Prioridad de Promesas y setTimeout

Asumir que setTimeout(fn, 0) y Promise.resolve().then(fn)tienen la misma prioridad porque ambos son "asíncronos" es incorrecto. Las promesas usan la Microtask Queue, que se procesa completamente después de cada tick de la Call Stack, mientras que setTimeout usa la Task Queue, que solo se procesa después de que la Microtask Queue esté vacía.

error-prioridad-promesas.js
Loading code...

Aunque el delay sea el mismo (0ms), la promesa siempre se ejecuta primero. Este comportamiento es por diseño: el Event Loop garantiza que todas las microtasks se procesen antes de tomar la siguiente macrotask de la cola. Esto significa que si una promesa crea otra promesa, y esta otra, todas se procesarán antes que cualquier setTimeout pendiente, sin importar cuándo fueron programadas.

Advertencia de Performance

No uses setTimeout(fn, 0) como solución para operaciones pesadas. Esto solo retrasa la ejecución al siguiente ciclo del Event Loop, pero sigue bloqueando el hilo principal cuando se ejecuta. Para trabajo pesado real, usa Web Workers (hilos separados) o divide el trabajo en chunks pequeños con setTimeout recursivo, permitiendo que el navegador renderice entre ejecuciones.

Resumen: Event Loop y Call Stack

Conceptos principales:

  • JavaScript es single-threaded y usa una Call Stack LIFO
  • El Event Loop coordina la ejecución síncrona y asíncrona
  • Las Web APIs manejan operaciones asíncronas en segundo plano
  • La Task Queue almacena callbacks de setTimeout, setInterval, I/O
  • La Microtask Queue tiene mayor prioridad y maneja promesas

Mejores prácticas:

  • Usa promesas o async/await en lugar de callbacks anidados
  • Evita operaciones síncronas pesadas que bloqueen el hilo principal
  • Para cálculos complejos, considera usar Web Workers
  • Entiende el orden de ejecución: síncrono → microtasks → macrotasks
  • Usa setTimeout con 0ms solo para delegar al siguiente ciclo del Event Loop