15 de diciembre de 2021

Microtareas (Microtasks)

Los manejadores o controladores (en adelante controladores) de promesas .then/.catch/.finally son siempre asincrónicos.

Incluso cuando una promesa es inmediatamente resuelta, el código en las líneas debajo de .then/.catch/.finally se ejecutará antes que estos controladores.

Veamos una demostración:

let promise = Promise.resolve();

promise.then(() => alert("¡Promesa realizada!"));

alert("código finalizado"); // esta alerta se muestra primero

Si ejecutas esto, verás código finalizado primero, y después ¡promesa realizada!.

Es algo extraño, porque la promesa se realiza por completo desde el principio.

¿Por qué .then se disparó después? ¿Qué está pasando?

Cola de microtareas (Microtasks queue)

Las tareas asincrónicas necesitan una gestión adecuada. Para ello, el estándar ECMA especifica una cola interna PromiseJobs, en ocasiones más conocida como “cola de microtareas” (término de V8).

Como se indica en la especificación:

  • La cola es first-in-first-out (FIFO), es decir, primero en entrar primero en salir: la tarea que entró primero en la cola, será la primera en ejecutarse.
  • La ejecución de una tarea se inicia sólo cuando no se está ejecutando nada más.

O, en palabras más simples, cuando una promesa está lista, sus controladores .then/catch/finally se ponen en la cola; ellos aún no se ejecutan. Cuando el motor de Javascript se libera del código actual, toma una tarea de la cola y la ejecuta.

Es por eso que el “código finalizado” en el ejemplo anterior se muestra primero.

Los controladores de promesas siempre pasan por esta cola interna.

Si hay una cadena con múltiples .then/catch/finally, entonces cada uno de ellos se ejecuta de forma asincrónica. Es decir, primero se pone en la cola, luego se ejecuta cuando se completa el código actual y se finalizan los controladores previamente en la cola.

¿y si el orden es importante para nosotros? ¿Cómo podemos hacer que código finalizado se ejecute después de ¡promesa realizada!?

Fácil, solo ponlo en la cola con .then:

Promise.resolve()
  .then(() => alert("promesa realizada!"))
  .then(() => alert("código finalizado"));

Ahora el orden es el previsto.

Rechazo no controlado

Recuerdas el evento unhandledrejection del artículo Manejo de errores con promesas?

Ahora podemos ver exactamente cómo Javascript descubre que hubo un rechazo no controlado o unhandled rejection

Se produce un “rechazo no controlado” cuando no se maneja un error de promesa al final de la cola de microtareas.

Normalmente, si esperamos un error, agregamos .catch a la cadena de promesa para manejarlo:

let promise = Promise.reject(new Error("¡Promesa fallida!"));
promise.catch(err => alert('atrapado'));

// no se ejecuta: error controlado
window.addEventListener('unhandledrejection', event => alert(event.reason));

Pero si olvidas añadir el .catch, entonces, después de que la cola de microtareas esté vacía, el motor activa el evento:

let promise = Promise.reject(new Error("¡Promesa fallida!"));

// Promesa fallida!
window.addEventListener('unhandledrejection', event => alert(event.reason));

¿Qué pasa si controlamos el error más tarde? Como esto:

let promise = Promise.reject(new Error("¡Promesa fallida!"));
setTimeout(() => promise.catch(err => alert('atrapado')), 1000);

// Error: ¡Promesa fallida!
window.addEventListener('unhandledrejection', event => alert(event.reason));

Ahora si lo ejecutamos, veremos ¡Promesa fallida! primero y después atrapado.

Si no supiéramos acerca de la cola de microtareas podríamos preguntarnos: "¿Por qué se ejecutó el controlador unhandledrejection? ¡Capturamos y manejamos el error!

Pero ahora entendemos que unhandledrejection se genera cuando se completa la cola de microtareas: el motor examina las promesas y, si alguna de ellas está en el estado “rechazado”, entonces el evento se dispara.

En el ejemplo anterior, .catch agregado por setTimeout también se dispara. Pero lo hace más tarde, después de que unhandledrejection ya ha ocurrido, por lo que no cambia nada.

Resumen

El control de promesas siempre es asíncrono, ya que todas las acciones de promesa pasan por la cola interna de “PromiseJobs”, también llamada “cola de microtareas” (término de V8).

Entonces, los controladores .then/catch/finally siempre se llaman después de que el código actual ha finalizado.

Si necesitamos garantizar que un código se ejecute después de .then/catch/finally, podemos agregarlo a una llamada encadenada .then.

En la mayoría de los motores de Javascript, incluidos los navegadores y Node.js, el concepto de microtareas está estrechamente relacionado con el “bucle de eventos” o “event loop” y “macrotareas” o “macrotasks”. Como estos no tienen relación directa con las promesas, están cubiertos en otra parte del tutorial, en el artículo Loop de eventos: microtareas y macrotareas.

Mapa del Tutorial

Comentarios

lea esto antes de comentar…
  • Si tiene sugerencias sobre qué mejorar, por favor enviar una propuesta de GitHub o una solicitud de extracción en lugar de comentar.
  • Si no puede entender algo en el artículo, por favor explique.
  • Para insertar algunas palabras de código, use la etiqueta <code>, para varias líneas – envolverlas en la etiqueta <pre>, para más de 10 líneas – utilice una entorno controlado (sandbox) (plnkr, jsbin, codepen…)