Volver al curso

JavaScript Desde Cero: Tu Primer Lenguaje de Programación

leccion
15 / 22
beginner
8 horas
JavaScript Asíncrono

Callbacks y el Event Loop: Cómo JavaScript Maneja el Tiempo

Lectura
40 min~8 min lectura

Callbacks y el Event Loop: Cómo JavaScript Maneja el Tiempo

Objetivos de aprendizaje

Al finalizar esta lección serás capaz de:

  • Entender por qué JavaScript es single-threaded y qué implica eso
  • Explicar el Event Loop, Call Stack, Web APIs y Callback Queue
  • Crear y utilizar callbacks para manejar operaciones asíncronas
  • Reconocer el problema del "callback hell" y entender por qué necesitamos Promises
  • Utilizar setTimeout, setInterval y requestAnimationFrame correctamente

1. JavaScript es single-threaded: ¿qué significa?

JavaScript ejecuta código en un solo hilo (single-threaded). Esto significa que solo puede hacer UNA cosa a la vez. Pero entonces, ¿cómo puede manejar múltiples tareas como cargar datos de una API, escuchar clicks y animar elementos, todo al mismo tiempo?

La respuesta está en el Event Loop — el mecanismo que permite a JavaScript parecer multitarea sin serlo realmente.

Los componentes del Event Loop

Imaginá una cocina con un solo chef (JavaScript):

  1. Call Stack (pila de ejecución): Es la lista de tareas que el chef está cocinando. Solo puede cocinar un plato a la vez, y los hace en orden LIFO (último en entrar, primero en salir).

  2. Web APIs: Son los ayudantes de cocina. Cuando el chef necesita algo que tarda (hornear algo por 30 minutos), le pasa la tarea a un ayudante y sigue con otra cosa.

  3. Callback Queue (cola de callbacks): Cuando los ayudantes terminan, ponen el resultado en una cola. El chef revisa la cola cuando termina su tarea actual.

  4. Event Loop: Es el gerente que revisa constantemente: "¿El chef terminó? ¿Hay algo en la cola? Entonces pasale la siguiente tarea."

console.log("1. Inicio");

setTimeout(() => {
  console.log("2. Esto tarda 0ms (pero va a la cola)");
}, 0);

console.log("3. Fin");

// Output:
// 1. Inicio
// 3. Fin
// 2. Esto tarda 0ms (pero va a la cola)

¿Sorprendido? Aunque el timeout es de 0ms, el callback va a la cola y se ejecuta DESPUÉS de que el código síncrono termine. Esto demuestra que el Event Loop siempre prioriza el Call Stack sobre la Callback Queue.

Visualizando el Event Loop paso a paso

console.log("A");

setTimeout(() => console.log("B"), 1000);

setTimeout(() => console.log("C"), 0);

console.log("D");

Paso 1: console.log("A") entra al Call Stack → se ejecuta → "A"

Paso 2: setTimeout(B, 1000) entra al Call Stack → se pasa a Web APIs (timer de 1s) → Call Stack vacío

Paso 3: setTimeout(C, 0) entra al Call Stack → se pasa a Web APIs (timer de 0ms) → Call Stack vacío

Paso 4: console.log("D") entra al Call Stack → se ejecuta → "D"

Paso 5: Timer de C termina → callback de C va a la Callback Queue

Paso 6: Event Loop ve Call Stack vacío → mueve callback de C al Call Stack → "C"

Paso 7: Después de 1 segundo, timer de B termina → callback de B va a la Queue → Event Loop lo ejecuta → "B"

Output final: A, D, C, B


2. Callbacks: funciones como argumentos

Un callback es simplemente una función que se pasa como argumento a otra función, para ser ejecutada después de que algo ocurra.

Callbacks síncronos

Ya los usaste sin saberlo:

// forEach recibe un callback
[1, 2, 3].forEach((num) => {
  console.log(num); // Este callback se ejecuta síncronamente
});

// map recibe un callback
const dobles = [1, 2, 3].map(n => n * 2);

// sort recibe un callback
const ordenado = [3, 1, 2].sort((a, b) => a - b);

// Nuestro propio callback síncrono
function procesarArray(arr, callback) {
  const resultado = [];
  for (const item of arr) {
    resultado.push(callback(item));
  }
  return resultado;
}

const cuadrados = procesarArray([1, 2, 3, 4], n => n ** 2);
console.log(cuadrados); // [1, 4, 9, 16]

Callbacks asíncronos

Estos se ejecutan "más tarde", cuando una operación termina:

// setTimeout: ejecutar después de X milisegundos
setTimeout(() => {
  console.log("Esto se ejecuta después de 2 segundos");
}, 2000);

// setInterval: ejecutar cada X milisegundos
let contador = 0;
const intervalo = setInterval(() => {
  contador++;
  console.log(`Tick ${contador}`);
  
  if (contador >= 5) {
    clearInterval(intervalo); // Detener el intervalo
    console.log("Intervalo detenido");
  }
}, 1000);

// Event listeners (son callbacks asíncronos)
document.addEventListener("click", (e) => {
  console.log("Click!"); // Se ejecuta "cuando" ocurre el evento
});

Callbacks para operaciones secuenciales

// Simular una operación que tarda (como leer un archivo o llamar a una API)
function obtenerUsuario(id, callback) {
  console.log(`Buscando usuario ${id}...`);
  setTimeout(() => {
    const usuario = { id, nombre: "Ana", email: "[email protected]" };
    callback(null, usuario); // Convención: (error, resultado)
  }, 1000);
}

function obtenerPedidos(usuarioId, callback) {
  console.log(`Buscando pedidos del usuario ${usuarioId}...`);
  setTimeout(() => {
    const pedidos = [
      { id: 1, producto: "Laptop", total: 999 },
      { id: 2, producto: "Mouse", total: 29 },
    ];
    callback(null, pedidos);
  }, 1000);
}

// Usar los callbacks
obtenerUsuario(1, (error, usuario) => {
  if (error) {
    console.error("Error:", error);
    return;
  }
  console.log("Usuario encontrado:", usuario.nombre);
  
  obtenerPedidos(usuario.id, (error, pedidos) => {
    if (error) {
      console.error("Error:", error);
      return;
    }
    console.log(`${usuario.nombre} tiene ${pedidos.length} pedidos`);
  });
});

3. El problema del Callback Hell

Cuando necesitás encadenar muchas operaciones asíncronas, los callbacks se anidan cada vez más profundo:

// ❌ Callback Hell ("Pyramid of Doom")
obtenerUsuario(1, (err, usuario) => {
  if (err) return manejarError(err);
  obtenerPedidos(usuario.id, (err, pedidos) => {
    if (err) return manejarError(err);
    obtenerDetallesPedido(pedidos[0].id, (err, detalles) => {
      if (err) return manejarError(err);
      calcularEnvio(detalles.direccion, (err, envio) => {
        if (err) return manejarError(err);
        procesarPago(detalles.total + envio.costo, (err, pago) => {
          if (err) return manejarError(err);
          enviarConfirmacion(pago.id, (err, confirmacion) => {
            if (err) return manejarError(err);
            console.log("¡Todo listo!", confirmacion);
          });
        });
      });
    });
  });
});

Este código es:

  • Difícil de leer
  • Difícil de mantener
  • Difícil de manejar errores
  • Propenso a bugs

Este problema es tan conocido que tiene un sitio web dedicado: callbackhell.com.

La solución a este problema son las Promises, que veremos en la siguiente lección.

Mitigando el callback hell (parcialmente)

// Estrategia: funciones nombradas en vez de anónimas
function manejarUsuario(err, usuario) {
  if (err) return manejarError(err);
  console.log("Usuario:", usuario.nombre);
  obtenerPedidos(usuario.id, manejarPedidos);
}

function manejarPedidos(err, pedidos) {
  if (err) return manejarError(err);
  console.log(`${pedidos.length} pedidos encontrados`);
  // ...
}

obtenerUsuario(1, manejarUsuario);
// Más plano, pero todavía limitado

Errores comunes de principiantes
  1. Pensar que setTimeout es preciso: setTimeout(fn, 1000) NO garantiza ejecución exacta a los 1000ms. Garantiza un MÍNIMO de 1000ms.

  2. No guardar la referencia de setInterval: Sin clearInterval(), el intervalo corre para siempre. Siempre guardá el ID retornado.

  3. Intentar retornar valores desde callbacks asíncronos: return dentro de un callback no retorna al scope externo. Necesitás otro callback o una Promise.

  4. No manejar errores en callbacks: Siempre verificá el primer argumento (convención error-first: callback(error, resultado)).

  5. Confundir código síncrono con asíncrono: Si esperás un resultado inmediato de una operación asíncrona, vas a obtener undefined.


Puntos clave de esta lección
  1. JavaScript es single-threaded pero maneja operaciones asíncronas gracias al Event Loop.
  2. El Call Stack ejecuta código síncrono; las Web APIs manejan tareas asíncronas en segundo plano.
  3. La Callback Queue almacena callbacks listos; el Event Loop los mueve al Call Stack cuando está vacío.
  4. Un callback es una función pasada como argumento que se ejecuta cuando algo termina.
  5. La convención es error-first: callback(error, resultado).
  6. El Callback Hell ocurre al anidar muchos callbacks — se resuelve con Promises y async/await.
  7. setTimeout con 0ms no es instantáneo — va a la cola y se ejecuta después del código síncrono.

Quiz de autoevaluación

1. ¿Qué significa que JavaScript es single-threaded?
a) Solo puede ejecutar una tarea a la vez
b) Solo funciona en un navegador
c) Solo tiene un archivo
d) Solo usa un core del procesador

2. ¿Cuál es el orden de ejecución de: console.log('A'), setTimeout(() => console.log('B'), 0), console.log('C')?
a) A, B, C
b) A, C, B
c) B, A, C
d) C, A, B

3. ¿Qué es el Callback Hell?
a) Un error de JavaScript
b) Callbacks anidados excesivamente que hacen el código ilegible
c) Una función que nunca termina
d) Un tipo de loop infinito

4. ¿Qué hace clearInterval()?
a) Limpia la consola
b) Detiene un setInterval previamente creado
c) Resetea un timer
d) Elimina un callback

5. ¿Cuál es la convención error-first en callbacks?
a) Los errores se ignoran
b) El primer argumento del callback es el error (null si no hubo)
c) Se lanza una excepción
d) El error va al final

Respuestas: 1-a, 2-b, 3-b, 4-b, 5-b


💡 Concepto Clave

Revisemos los puntos más importantes de esta lección antes de continuar.

Ejercicio práctico

Misión: Simulador de restaurante con callbacks

Creá funciones que simulen el proceso de un restaurante usando callbacks y setTimeout:

  1. tomarPedido(mesa, platos, callback) — tarda 1 segundo
  2. prepararPlato(plato, callback) — tarda 2-4 segundos (aleatorio)
  3. servirPlato(mesa, plato, callback) — tarda 0.5 segundos
  4. cobrar(mesa, total, callback) — tarda 1 segundo

El flujo debe ser: tomar pedido → preparar cada plato → servir cada plato → cobrar.
Mostrá logs con timestamps para ver el orden de ejecución.

const pedido = {
  mesa: 5,
  platos: ["Empanadas", "Asado", "Flan"],
  total: 2500
};
// Implementá el flujo completo
🧠 Pon a prueba tu conocimiento
¿Cuál es el aspecto más importante que aprendiste en esta lección?
  • Comprendo el concepto principal y puedo explicarlo con mis palabras
  • Entiendo cómo aplicarlo en mi situación específica
  • Necesito repasar algunas partes antes de continuar
  • Quiero ver más ejemplos prácticos del tema
✅ ¡Excelente! Continúa con la siguiente lección para profundizar más.