← JavaScript

Asincronía

¿Cómo funciona `setTimeout` y por qué el retraso que le pasas no es exacto?

Ver respuesta — intenta responderla en voz alta primero

setTimeout programa una función para que se ejecute después de un tiempo mínimo, no exacto. El callback se coloca en la cola de macrotareas y solo se ejecuta cuando el call stack está vacío y las microtareas ya se procesaron. Por eso el retraso es un mínimo garantizado, no una promesa de precisión.

setTimeout(callback, ms) no ejecuta el callback exactamente a los ms milisegundos. Lo que hace es: esperar ese tiempo mínimo y luego colocar el callback en la cola de macrotareas. El callback solo se ejecuta cuando:

  1. El código síncrono actual ha terminado (call stack vacío).
  2. Todas las microtareas pendientes (promesas) se han procesado.

Por eso setTimeout(fn, 0) no se ejecuta de inmediato: espera a que todo el código síncrono termine.

// El retraso es un mínimo, no exacto
console.log("inicio");
setTimeout(() => console.log("después de ~0ms"), 0);
console.log("fin");
// Salida: inicio, fin, después de ~0ms

// El código síncrono retrasa el timeout incluso cuando está en 0ms
setTimeout(() => console.log("timeout"), 0);
for (let i = 0; i < 1e9; i++) {} // bucle pesado y bloqueante
console.log("bucle terminado");
// "bucle terminado" aparece ANTES que "timeout",
// porque el timeout espera a que se libere el hilo.

// El bug clásico: var en un bucle con setTimeout
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// Imprime 3, 3, 3 (con var, todos comparten el mismo i)

// Con let se arregla: cada iteración tiene su propio i
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0);
}
// Imprime 0, 1, 2
  • Creer que setTimeout(fn, 1000) se ejecuta exactamente al segundo. Si el hilo está ocupado, se retrasa.
  • El bug del bucle con var: como var no tiene alcance de bloque, todos los callbacks comparten la misma variable, que para cuando se ejecutan ya tiene el valor final. Se arregla con let.

"setTimeout programa un callback para que se ejecute después de un tiempo mínimo, no exacto. Cuando pasa ese tiempo, el callback va a la cola de macrotareas y solo se ejecuta cuando el call stack está vacío y las microtareas ya se procesaron. Por eso un setTimeout de 0 no se ejecuta de inmediato: espera a que el código síncrono termine. Un caso clásico es el bucle con var y setTimeout, que imprime el valor final repetido porque todas las iteraciones comparten la misma variable; se arregla usando let."

Reto rápido

¿Qué imprime esto y cómo lo arreglarías para que imprima 0, 1, 2?

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
Ver respuesta

Imprime 3, 3, 3. Se arregla cambiando var por let, que crea una nueva variable i en cada iteración, de modo que cada callback recuerda su propio valor e imprime 0, 1, 2.