Promesas y event loop en JavaScript, medidos en jsc

Curso de JavaScript 5 en jsc: microtareas vs setTimeout, rechazos que desaparecen sin aviso, Promise.all vs allSettled y 311 vs 124 ms de await.

Serie: curso de JavaScript 5/6 (avanzado). Esta clase es para ti si ya usas async/await y APIs del estilo de fetch, y alguna vez te mordió código que se ejecutó "en el orden equivocado", un catch que nunca saltó o una página que se congeló mientras una promesa corría "en segundo plano". Le pasa al estudiante de bootcamp que conecta su primer frontend con una API, al freelancer que integra una pasarela de pagos y al dev junior que hereda un servicio en Node en una startup. Al terminar vas a predecir el orden exacto del código síncrono, las microtareas y los temporizadores, elegir entre Promise.all, allSettled, race y any, y entender por qué un await en un bucle a veces es tres veces más lento de lo necesario.

La idea central cabe en una línea: JavaScript ejecuta tu código en un solo hilo, y async solo cambia cuándo se ejecuta un fragmento, nunca dónde. Todo lo que sigue es un archivo pequeño que ejecutamos, con su salida y sus tiempos reales. También descubrimos que la consola que usamos se traga los rechazos de promesas sin manejar sin decir una palabra, y justamente por eso el código de producción necesita un hábito que no dependa del entorno.

Entorno: JavaScriptCore de macOS 26.6.2 (versión del framework 21624.5.1.11.3) mediante su consola jsc, Mac con Apple Silicon (M1 Pro). No se ejecutaron Node.js, Deno ni navegadores. Primero comprobamos en jsc: existen Promise, setTimeout, performance.now(), Promise.withResolvers, Promise.try y for await; no existen queueMicrotask, clearTimeout, setInterval, setImmediate, process, AbortController ni fetch. El alias de la clase 1: alias jsc=/System/Library/Frameworks/JavaScriptCore.framework/Versions/Current/Helpers/jsc.

De callbacks a promesas y a async/await

Las APIs antiguas (y la biblioteca base de Node) entregan el resultado con un callback, con el error primero: cb(err, value). Una promesa es un objeto que representa un resultado que llegará después y que termina cumplida con un valor o rechazada con un error. async/await es sintaxis encima de las promesas. Aquí está el mismo trabajo de dos pasos escrito de las tres formas:

// callbacks.js — the same three steps: callbacks, then Promise, then async/await
// A fake error-first callback API (the Node.js style: cb(err, value))
function getUser(id, cb)     { setTimeout(() => id > 0 ? cb(null, { id, name: "user" + id }) : cb(new Error("bad id")), 20); }
function getOrders(user, cb) { setTimeout(() => cb(null, [user.id * 10, user.id * 10 + 1]), 20); }

// 1) callbacks: every step nests, every step must check err
getUser(7, (err, user) => {
  if (err) return print("cb error:", err.message);
  getOrders(user, (err, orders) => {
    if (err) return print("cb error:", err.message);
    print("callbacks:", user.name, orders);
  });
});

// 2) wrap once, then chain
const promisify = (fn) => (...args) => new Promise((res, rej) => fn(...args, (err, v) => err ? rej(err) : res(v)));
const getUserP = promisify(getUser), getOrdersP = promisify(getOrders);
getUserP(7).then(u => getOrdersP(u).then(o => print("promise:  ", u.name, o)))
           .catch(e => print("promise error:", e.message));

// 3) async/await reads top to bottom; one try/catch covers both steps
(async () => {
  try { const u = await getUserP(7); const o = await getOrdersP(u); print("async:    ", u.name, o); }
  catch (e) { print("async error:", e.message); }
  try { await getUserP(-1); } catch (e) { print("async error:", e.message); }
})();
$ jsc callbacks.js
callbacks: user7 70,71
promise:   user7 70,71
async:     user7 70,71
async error: bad id

La versión con callbacks necesita comprobar el error en cada nivel y crece hacia la derecha con cada paso (el famoso "callback hell"). El envoltorio promisify (Node trae uno, util.promisify; no ejecutado aquí) convierte una sola vez cualquier API con error primero. Con await, los errores de los dos pasos caen en un solo try/catch, que es la gran ventaja práctica.

await solo se permite dentro de una función async o en el nivel superior de un módulo. En un script normal jsc da un mensaje del parser confuso, porque ahí await es solo un identificador:

$ jsc awaitbad.js        # function f() { const x = await Promise.resolve(1); }
Exception: SyntaxError: Unexpected identifier 'Promise'. Expected ';' after variable declaration.
at awaitbad.js:2
$ jsc tla.js             # const v = await Promise.resolve(1); print(v);
Exception: SyntaxError: Unexpected identifier 'Promise'. Expected ';' after variable declaration.
at tla.js:1
$ cp tla.js tla.mjs && jsc tla.mjs
1

El orden, impreso: síncrono, luego microtareas, luego temporizadores

Adivina el orden de las letras antes de leer la salida:

// order.js — who runs first? Guess the order before running.
print("A sync start");
setTimeout(() => print("B setTimeout 0"), 0);
setTimeout(() => print("C setTimeout 0 (second)"), 0);
Promise.resolve().then(() => print("D then #1"))
                 .then(() => print("E then #1 -> then"));
Promise.resolve().then(() => {
  print("F then #2");
  Promise.resolve().then(() => print("G microtask queued by a microtask"));
});
(async () => {
  print("H async fn body (sync part)");
  await null;
  print("I after await null");
})();
new Promise(resolve => { print("J executor"); resolve(); });
print("K sync end");
$ jsc order.js
A sync start
H async fn body (sync part)
J executor
K sync end
D then #1
F then #2
I after await null
E then #1 -> then
G microtask queued by a microtask
B setTimeout 0
C setTimeout 0 (second)

Las reglas que demuestra esta salida:

  1. El código síncrono se ejecuta primero hasta el final, y eso incluye el cuerpo de una función async hasta su primer await (H) y la función ejecutora de una promesa (J). Ninguno de los dos es "asíncrono" por sí mismo.
  2. Después se vacía la cola de microtareas. Una microtarea es un trabajo de seguimiento corto: un callback de then o el resto de una función async después de await. D, F e I se encolaron durante el script, así que se ejecutan en ese orden.
  3. Las microtareas encoladas por microtareas también van antes que cualquier temporizador. E (encolada cuando terminó D) y G (encolada dentro de F) se ejecutaron antes que B.
  4. Recién entonces se ejecuta un callback de temporizador (B, C), y después de cada uno se vuelve a vaciar toda la cola de microtareas.
Diagrama de flujo. Primero se ejecuta el código síncrono, incluidas las funciones ejecutoras de Promise y la parte de una función async anterior a su primer await. Luego el loop comprueba si la cola de microtareas está vacía. Si no, ejecuta una microtarea, como un callback de then o el código después de un await, y vuelve a comprobar. Cuando está vacía, ejecuta un callback de temporizador vencido de setTimeout y vuelve a la comprobación de microtareas.
Una vuelta del loop tal como se observó en jsc: el script se ejecuta hasta el final, corren todas las microtareas (incluidas las encoladas por microtareas) y recién entonces el siguiente callback de temporizador.

En los navegadores valen las mismas reglas para las promesas, y además el navegador puede repintar entre tareas; Node.js agrega sus propias fases y process.nextTick, que corre antes que las microtareas de promesas. No ejecutamos ninguno de los dos; el orden de jsc de arriba es el que especifica la cola de jobs de ECMAScript, más la lista de temporizadores simple de jsc.

Promise.all, allSettled, race y any con peticiones falsas cronometradas

Cuatro combinadores responden cuatro preguntas distintas. Le dimos a cada uno las mismas tres peticiones falsas (100 ms bien, 30 ms con fallo o bien, 60 ms bien) y medimos cuánto tardaba en resolverse:

// combinators.js — all / allSettled / race / any with timed fake requests
const sleep = (ms, v, fail = false) => new Promise((res, rej) =>
  setTimeout(() => fail ? rej(new Error(`${v} failed`)) : res(v), ms));
let t0;
const step = () => `${Math.round(performance.now() - t0)} ms`;
async function main() {
  t0 = performance.now();
  try { await Promise.all([sleep(100, "a"), sleep(30, "b", true), sleep(60, "c")]); }
  catch (e) { print("all        -> rejected:", e.message, "after", step()); }
  t0 = performance.now();
  const s = await Promise.allSettled([sleep(100, "a"), sleep(30, "b", true), sleep(60, "c")]);
  print("allSettled ->", JSON.stringify(s), "after", step());
  t0 = performance.now();
  print("race       ->", await Promise.race([sleep(100, "a"), sleep(30, "b"), sleep(60, "c")]), "after", step());
  t0 = performance.now();
  print("any        ->", await Promise.any([sleep(100, "a"), sleep(30, "b", true), sleep(60, "c")]), "after", step());
  try { await Promise.any([sleep(10, "x", true), sleep(20, "y", true)]); }
  catch (e) { print("any (all failed) ->", e.name, JSON.stringify(e.message), e.errors.map(x => x.message)); }
}
main();
$ jsc combinators.js
all        -> rejected: b failed after 32 ms
allSettled -> [{"status":"fulfilled","value":"a"},{"status":"rejected","reason":{}},{"status":"fulfilled","value":"c"}] after 104 ms
race       -> b after 33 ms
any        -> c after 63 ms
any (all failed) -> AggregateError "" x failed,y failed
  • Promise.all falla rápido: se rechazó a los 32 ms con el primer error, sin esperar a las demás. Las otras dos siguieron corriendo de todas formas; nada las cancela.
  • allSettled siempre espera a todas (104 ms) y nunca se rechaza. Fíjate en la trampa de la salida: "reason":{}. JSON.stringify de un Error da {} porque message y stack no son propiedades propias enumerables. Registra reason.message, no el JSON.
  • race se resuelve con la primera que termine, sea éxito o fallo. any espera el primer éxito (63 ms, saltándose el fallo de los 30 ms) y solo se rechaza cuando todas fallan, con un AggregateError cuyo message fue un string vacío en jsc y cuyo arreglo errors guarda las causas.

Un timeout armado con race deja de esperar, no de trabajar:

// timeout.js — race() gives up waiting, but the slow work keeps running
const slowJob = () => new Promise(res => setTimeout(() => { print("slow job finished anyway"); res("data"); }, 200));
const timeout = (ms) => new Promise((_, rej) => setTimeout(() => rej(new Error(`timeout after ${ms}ms`)), ms));
Promise.race([slowJob(), timeout(50)])
  .then(v => print("got", v), e => print("race:", e.message));
$ jsc timeout.js
race: timeout after 50ms
slow job finished anyway

La cancelación de verdad necesita que la operación coopere: en navegadores y Node, fetch acepta una señal de AbortController (no disponible en jsc, así que no se ejecutó aquí).

Error común 1: un rechazo sin manejar que desaparece

Lo que se suele escribir: una llamada async sin await ni catch, normalmente un registro de logs o de analítica del tipo "dispara y olvida".

// reject.js — what does jsc do with an unhandled rejection?
print("before");
Promise.reject(new Error("nobody catches me"));
setTimeout(() => print("timer still runs"), 10);
print("after");

Qué pasa en realidad en jsc:

$ jsc reject.js; echo "exit=$?"
before
after
timer still runs
exit=0
$ jsc throw_async.js; echo "exit=$?"     # async function load() { throw new TypeError("bad json"); } load();
main finished
exit=0
$ jsc -e 'throw new Error("sync")'; echo "exit=$?"
Exception: Error: sync
global code@[Command Line]:1:16
exit=3

El error síncrono imprimió Exception y código de salida 3. El asíncrono no imprimió nada y salió con 0: en este entorno el error simplemente se pierde. Los navegadores muestran Uncaught (in promise) en la consola y disparan un evento unhandledrejection; Node.js 15 o posterior tumba el proceso con código de salida 1 por defecto. No ejecutamos ninguno de los dos, así que revisa tu propio runtime; lo importante es que el comportamiento lo define el host, y "no pasó nada" en un entorno puede ser una caída en otro.

La consola jsc tiene un gancho, setUnhandledRejectionCallback (exclusivo de jsc, no estándar), que hace visibles los rechazos. Recibe la promesa, así que leemos el motivo con un catch:

// jsc-only: setUnhandledRejectionCallback is a jsc shell helper, not standard JS.
// It receives the rejected promise; we read the reason with a catch.
setUnhandledRejectionCallback(p => p.catch(e => print("UNHANDLED:", e)));
async function load() { throw new TypeError("bad json"); }
load();
const late = Promise.reject(new Error("caught later"));
setTimeout(() => late.catch(e => print("handled:", e.message)), 0);
print("main finished");
$ jsc unhandled_hook.js
main finished
UNHANDLED: TypeError: bad json
UNHANDLED: Error: caught later
handled: caught later

La segunda línea es sutil: un rechazo que recibe su manejador más tarde (en un temporizador) primero se reporta como no manejado, porque la comprobación corre después de vaciar las microtareas. Los navegadores tienen un evento rejectionhandled para este caso.

Solución: cada promesa se espera con await dentro de try/catch o termina en .catch(...). Para un "dispara y olvida" deliberado, escribe logEvent().catch(reportError) para que la intención quede a la vista.

Error común 2: forEach con async, y return sin await

Lo que se suele escribir: array.forEach(async item => { await save(item); }) y después usar los resultados.

// foreach.js — forEach does not wait for async callbacks
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const ids = [3, 1, 2];
async function main() {
  const out = [];
  ids.forEach(async id => { await sleep(id * 10); out.push(id); });
  print("forEach returned, out =", JSON.stringify(out));
  await sleep(50);
  print("50 ms later,     out =", JSON.stringify(out));   // finishing order

  const all = await Promise.all(ids.map(async id => { await sleep(id * 10); return id; }));
  print("map + Promise.all    =", JSON.stringify(all));   // input order, run in parallel

  const seq = [];
  for (const id of ids) { await sleep(id * 10); seq.push(id); }
  print("for...of + await     =", JSON.stringify(seq));   // input order, one at a time
}
main();
$ jsc foreach.js
forEach returned, out = []
50 ms later,     out = [1,2,3]
map + Promise.all    = [3,1,2]
for...of + await     = [3,1,2]

Qué pasa en realidad: forEach ignora las promesas que devuelve su callback, así que volvió con el arreglo vacío y los resultados llegaron después en el orden en que terminaron. Solución: map + Promise.all (en paralelo, en el orden de entrada) o for...of + await (de a una).

La segunda trampa es return sin await dentro de try:

const fetchUser = async () => { throw new Error("404 user"); };
async function noAwait() {
  try { return fetchUser(); }            // returns the promise; try is already finished
  catch (e) { return "fallback (noAwait)"; }
}
async function withAwait() {
  try { return await fetchUser(); }      // the rejection is thrown here, inside try
  catch (e) { return "fallback (withAwait)"; }
}
noAwait().then(v => print("noAwait:", v), e => print("noAwait rejected:", e.message));
withAwait().then(v => print("withAwait:", v));
$ jsc returnawait.js
withAwait: fallback (withAwait)
noAwait rejected: 404 user

Sin await, la función le entrega a quien la llamó la promesa todavía pendiente y sale del bloque try; el rechazo ocurre fuera de él y el plan B nunca se ejecuta. Dentro de try, escribe return await.

Medición: await secuencial contra paralelo

Tres esperas independientes (en lugar de tres llamadas a una API) de 120, 80 y 100 ms, cinco ejecuciones cada una:

// seqpar.js — three independent "requests" of 120, 80 and 100 ms
const sleep = (ms, v) => new Promise(r => setTimeout(() => r(v), ms));
const DELAYS = [120, 80, 100];
async function sequential() { const out = []; for (const d of DELAYS) out.push(await sleep(d, d)); return out; }
async function parallel()   { return Promise.all(DELAYS.map(d => sleep(d, d))); }
async function startThenAwait() {            // start all first, await later: also parallel
  const a = sleep(120, 120), b = sleep(80, 80), c = sleep(100, 100);
  return [await a, await b, await c];
}
async function time(name, fn) {
  const runs = [];
  for (let i = 0; i < 5; i++) { const t = performance.now(); await fn(); runs.push(performance.now() - t); }
  runs.sort((x, y) => x - y);
  print(`${name.padEnd(16)} median=${runs[2].toFixed(1)} ms  runs=${runs.map(r => r.toFixed(1)).join(", ")}`);
}
(async () => { await time("sequential", sequential); await time("Promise.all", parallel); await time("start-then-await", startThenAwait); })();
$ jsc seqpar.js
sequential       median=311.6 ms  runs=308.7, 311.5, 311.6, 312.5, 318.0
Promise.all      median=123.9 ms  runs=123.2, 123.7, 123.9, 124.1, 124.2
start-then-await median=122.5 ms  runs=122.0, 122.2, 122.5, 123.0, 124.2
Gráfico de barras. Esperar las tres una tras otra tardó 311.6 milisegundos, la suma de las esperas. Promise.all tardó 123.9 milisegundos y arrancar las tres antes de esperarlas tardó 122.5 milisegundos, más o menos la espera individual más larga.
Tres esperas independientes de 120, 80 y 100 ms. Mediana de 5 ejecuciones, jsc en macOS 26.6.2, Apple M1 Pro.

La versión secuencial es la suma de las esperas (300 ms más el overhead); la paralela es la más larga (120 ms más el overhead). El overhead de unos 4 ms por cada 100 ms de espera es el retraso de los temporizadores en esta consola; lo medimos con 20 muestras por espera:

$ jsc timer_accuracy.js
delay   0 ms: late by median 0.04 ms, max 0.10 ms
delay   1 ms: late by median 0.18 ms, max 0.32 ms
delay  10 ms: late by median 0.72 ms, max 1.80 ms
delay 100 ms: late by median 3.98 ms, max 11.52 ms

setTimeout(fn, d) significa "no antes de d ms", nunca "exactamente a los". Además, los navegadores suben los temporizadores anidados a un mínimo de 4 ms y frenan las pestañas en segundo plano (no ejecutado aquí). Fíjate en que start-then-await también es paralelo: lo que importa es cuándo se crea la promesa, no cuándo se espera. Usa la forma secuencial solo cuando cada llamada necesita el resultado anterior o cuando tienes que limitar la carga sobre un servidor.

Medición: qué bloquea un temporizador

Como hay un solo hilo, un temporizador solo puede dispararse cuando el hilo está libre. Programamos un temporizador de 10 ms y luego mantuvimos ocupado el hilo de tres formas:

// blocking.js — when does a 10 ms timer fire while other work runs?
// usage: jsc blocking.js -- busy | microtasks | chunked
const which = arguments[0];
const t0 = performance.now();
const ms = () => (performance.now() - t0).toFixed(1);
const spin = (dur) => { const end = performance.now() + dur; while (performance.now() < end) {} };
setTimeout(() => print(`${which}: 10 ms timer fired at ${ms()} ms`), 10);
if (which === "busy") {
  spin(200);                                    // 200 ms of synchronous work in one go
  print(`${which}: work done at ${ms()} ms`);
} else if (which === "microtasks") {
  let n = 0;                                    // 2,000,000 chained microtasks
  const loop = () => { if (++n < 2_000_000) Promise.resolve().then(loop); else print(`${which}: work done at ${ms()} ms`); };
  loop();
} else if (which === "chunked") {
  let slices = 0;                               // the same 200 ms, in 20 slices of 10 ms
  const loop = () => { spin(10); if (++slices < 20) setTimeout(loop, 0); else print(`${which}: work done at ${ms()} ms`); };
  loop();
}

La primera de tres rondas:

$ for w in busy microtasks chunked; do jsc blocking.js -- $w; done
busy: work done at 200.1 ms
busy: 10 ms timer fired at 200.3 ms
microtasks: work done at 102.3 ms
microtasks: 10 ms timer fired at 102.5 ms
chunked: 10 ms timer fired at 11.7 ms
chunked: work done at 203.4 ms
Trabajo en curso El temporizador saltó a los (mediana de 3) El trabajo terminó a los
Bucle síncrono de 200 ms 200.3 ms 200.1 ms
2,000,000 de microtareas encadenadas 102.5 ms ≈102 ms
Los mismos 200 ms en porciones de 10 ms con setTimeout 11.7 ms ≈203 ms
Gráfico de barras frente a un objetivo de 10 milisegundos. Durante 200 milisegundos de trabajo síncrono, el temporizador saltó a los 200.3 milisegundos. Durante dos millones de microtareas encadenadas saltó a los 102.5 milisegundos. Cuando el mismo trabajo de 200 milisegundos se dividió en porciones de 10 milisegundos con setTimeout, saltó a los 11.7 milisegundos.
Cuándo se disparó de verdad un setTimeout(fn, 10) mientras corría otro trabajo. Mediana de 3 ejecuciones, jsc en macOS 26.6.2, Apple M1 Pro.

La fila de microtareas sorprende a mucha gente: "es async, así que cede el paso" es falso. Una cadena de microtareas no deja entrar a ningún temporizador (ni, en un navegador, a un repintado o un manejador de clic) hasta que toda la cadena termina. Solo ceder el paso a la cola de tareas (setTimeout, o scheduler.yield()/MessageChannel en navegadores, no ejecutado aquí) le da turno a otro trabajo. El precio de dividir fue de unos 3 ms en el tiempo total. Para trabajo de CPU demasiado largo incluso dividido, sácalo del hilo: un Web Worker en navegadores, worker_threads en Node (ninguno ejecutado aquí), igual que la clase de Dart lleva trabajo a un isolate (en inglés).

Tabla de decisión

Necesitas Usa Cuidado con
Un resultado tras otro, cada uno depende del anterior for...of + await El tiempo total es la suma
Varios resultados independientes, todos obligatorios Promise.all Falla rápido; las demás siguen corriendo
Varios resultados, se tolera un fallo parcial Promise.allSettled Nunca se rechaza; revisa cada status
El primer éxito entre espejos o alternativas Promise.any AggregateError con errors si todas fallan
Un límite de tiempo Promise.race con un timeout Deja de esperar, no detiene el trabajo; usa señales de abort donde existan
Un resolvedor que puedas llamar desde fuera Promise.withResolvers() (ES2024) Comprobado solo en jsc
Trabajo largo de CPU sin congelar Porciones con setTimeout, o un worker Las cadenas de microtareas no ceden el paso

Promise.withResolvers() devolvió { promise, resolve, reject } y Promise.try(fn) convirtió un throw síncrono en un rechazo en este jsc; los dos son agregados recientes, así que revisa tus entornos de destino antes de depender de ellos.

Ejercicios

  1. Predice la salida y luego ejecútalo: setTimeout(() => print(1), 0); (async () => { print(2); await undefined; print(3); })(); Promise.resolve().then(() => print(4)); print(5);
  2. loadAll(ids) tiene que pedir cada id con get(id) (que devuelve una promesa), como máximo dos a la vez, y devolver los resultados en el orden de entrada. Escríbelo sin bibliotecas.

Respuestas

  1. 2 5 3 4 1. 2 y 5 son síncronos; 3 (después de await) se encoló antes que 4; el temporizador va al final.
  2. Una solución, comprobada en jsc con get = id => sleep(id * 10, id) y [3, 1, 2, 5], devolvió [3,1,2,5] en 91.2 y 87.8 ms en dos ejecuciones (lo ideal: 80 ms, porque un worker hace 10 + 20 ms mientras el otro hace 30 ms, y luego uno de ellos hace 50 ms):
async function loadAll(ids, limit = 2) {
  const out = new Array(ids.length);
  let next = 0;
  async function worker() {
    while (next < ids.length) { const i = next++; out[i] = await get(ids[i]); }
  }
  await Promise.all(Array.from({ length: limit }, worker));
  return out;
}

next++ es seguro sin locks porque el código entre dos await nunca se ejecuta de forma concurrente en un solo hilo.

Límites honestos y notas de versión

  • Un host, un motor. Toda la salida es de la consola jsc de macOS 26.6.2. El orden de los jobs de promesas lo define ECMAScript y debería coincidir en todas partes; los temporizadores, el manejo de rechazos sin manejar y todo lo relativo al renderizado son comportamiento del host. jsc no tiene clearTimeout, setInterval ni queueMicrotask.
  • Versiones: Promise y las funciones flecha son de ES2015, async/await de ES2017, for await y Promise.prototype.finally de ES2018, Promise.allSettled de ES2020, Promise.any y AggregateError de ES2021, el await de nivel superior de ES2022, Promise.withResolvers de ES2024 y Promise.try de ES2025. Todo comprobado solo en este jsc.
  • No ejecutado aquí: Node.js (fases del event loop, process.nextTick, setImmediate, util.promisify, caída ante rechazos sin manejar), navegadores (renderizado, eventos unhandledrejection, mínimo de 4 ms, fetch, AbortController, Web Workers). Descrito solo a partir de especificaciones y documentación.
  • Tiempos: las esperas son temporizadores, no llamadas de red. Las peticiones reales agregan variación; lo que se traslada es la proporción entre secuencial y paralelo, no los milisegundos.

Anterior: Clase 4: clases, prototipos y módulos · Siguiente: Clase 6: rendimiento, memoria y números

Serie: 1 tipos e igualdad · 2 funciones, scope y closures · 3 objetos y arreglos · Relacionados: async, streams e isolates en Dart (en inglés) · hilos, procesos y asyncio en Python · la hoja de ruta para aprender a programar

Preguntas frecuentes

¿Por qué Promise.then se ejecuta antes que setTimeout(fn, 0)?

Porque los callbacks de then son microtareas y los de setTimeout son tareas de temporizador. Cuando termina el script actual, el motor ejecuta todas las microtareas en cola, incluidas las que encolan otras microtareas, antes de tomar el siguiente temporizador. En nuestra prueba, las siete líneas de promesas y await se imprimieron antes que cualquiera de los dos setTimeout(fn, 0).

¿Qué pasa con una promesa rechazada que nadie atrapa?

Depende del entorno (host). En la consola jsc no se imprime nada y el código de salida es 0, así que el error simplemente desaparece; un throw síncrono, en cambio, imprime Exception y sale con código 3. Los navegadores registran un error 'Uncaught (in promise)' y Node.js 15 o posterior termina el proceso por defecto (ambos descritos, no ejecutados aquí). Termina siempre una cadena de promesas con catch o espérala con await dentro de try/catch.

¿Usar await dentro de un bucle es lento?

Solo cuando las operaciones son independientes. Esperar una tras otra tres pausas de 120, 80 y 100 ms tardó 311.6 ms (mediana de 5); Promise.all sobre las mismas tres tardó 123.9 ms. Si cada paso necesita el resultado anterior, el await secuencial es lo correcto; si no, arráncalas todas y espera con Promise.all.

  • #javascript
  • #avanzado
  • #async
  • #promesas
  • #event loop
  • #jsc
  • #javascriptcore

t.laboratory

A technical blog that records real problems from the apps, servers and services we build and run — and the code that fixed them.

Acerca de · Apps

↑