Asynchrony in JavaScript
promises, await and four places where an AI gets it wrong
What it is
Asynchrony in JavaScript is arranged the same way as in Python (article
2.10): async, await, waiting instead of idling. But it has its own
terminology and its own traps, and an AI reproduces all four below
regularly.
And here is a direct parallel with chapter 6: Promise.all and
Promise.allSettled relate to each other exactly as gather and
gather(return_exceptions=True) do.
What it looks like in code
const fetchCity = (city, ms = 100) =>
new Promise((resolve, reject) =>
setTimeout(() => city === "Bergen"
? reject(new Error("server returned 500"))
: resolve(`${city}: +4`), ms));A promise is a promise of a result that will appear later. It has three
states: pending, fulfilled, rejected. resolve is success, reject is an
error.
Writing new Promise by hand is almost never necessary: promises come back
from fetch and from libraries. But you do need to know what one is — the
word comes up constantly.
1. Promise.all drops everything
await Promise.all([fetchCity("Oslo"), fetchCity("Bergen"), fetchCity("Tromsø")]);failed entirely: server returned 500The requests to Oslo and Tromsø succeeded — their results are lost. Exactly
the same as asyncio.gather without protection in Python.
2. Promise.allSettled keeps the successes
const r = await Promise.allSettled([...]);
for (const x of r)
console.log(x.status === "fulfilled" ? `ok: ${x.value}` : `error: ${x.reason.message}`);ok: Oslo: +4
error: server returned 500
ok: Tromsø: +4Each element is an object with a status. A successful one has a value, a
failed one a reason. Checking status is mandatory: otherwise the error
travels on as an ordinary value.
| What you need | What to take |
|---|---|
| All of them must succeed or there is no point | Promise.all |
| A partial result is better than nothing | Promise.allSettled |
| The first successful answer is enough | Promise.any |
| The first answer of any kind counts, even an error (a race against a timeout) | Promise.race |
3. forEach doesn't wait
The sneakiest of the four:
const done = [];
["Oslo", "Tromsø"].forEach(async c => { done.push(await fetchCity(c)); });
console.log("right after forEach:", done);right after forEach: []
after 300 ms: [ 'Oslo: +4', 'Tromsø: +4' ]The array is empty. forEach cannot wait for async functions: it starts
them all and moves on without waiting. The data does arrive — but later,
once the code that needed it has already finished.
The right way is an ordinary loop:
for (const c of ["Oslo", "Tromsø"]) ok.push(await fetchCity(c));after the loop: [ 'Oslo: +4', 'Tromsø: +4' ]Or Promise.all with map, if the requests are independent and you want
them in parallel.
4. A forgotten await
without await: Promise { <pending> }
with await: Oslo: +4Instead of a value, a promise object. In Python that is
<coroutine object>, here Promise { <pending> } — the same symptom, a
different word.
What to tell the AI
These three requests are independent. I need a partial result:
if one server doesn't answer, the rest should still come
through. Use Promise.allSettled and handle status.This code has a forEach with async inside. Rewrite it so the
results are actually awaited: for...of if the requests depend
on each other, or Promise.all with map if they don't.Add a 5-second timeout to each request with
AbortSignal.timeout. On a timeout, a console warning and skip,
not a crash.What to look for
Promise.all where a partial result is wanted. One error loses
everything.
forEach(async ...). Doesn't wait. Always a mistake if the result is
needed.
No .catch and no try. A rejected promise nobody caught gives an
UnhandledPromiseRejection — in Node that means the process dies.
Sequential awaits in a loop where parallel would do. Not an error, but
three times slower on three requests (article 2.10).
No timeout. A request can hang forever (article 6.4). Both the browser
and Node have AbortSignal.timeout(ms) for that.
.then() mixed in with await. It works; it reads badly. New code uses
await.
Cheat sheet
A promise is a promise of a result;
resolvesuccess,rejecterror.Promise.alldrops everything at the first error,allSettledkeeps the successes.With
allSettledcheckstatus:fulfilled→value,rejected→reason.forEach(async ...)doesn't wait. You wantfor...oforPromise.all(map).Promise { <pending> }in the output means a forgottenawait.An uncaught rejected promise kills the process in Node.