Kodokon kodokon.com

Async in depth: promises, async/await, error handling

Leverage the event loop, parallelize your promises correctly, and make asynchronous error handling reliable.

11 min · 3 questions

Open this lesson in Kodokon

The execution model relies on a macrotask queue (setTimeout, I/O) and a microtask queue (.then, await, queueMicrotask). After each task, the engine drains the microtask queue completely before moving on. Direct consequence: a resolved promise always runs before a setTimeout(fn, 0), and an overly long chain of microtasks can freeze rendering just as surely as a synchronous loop.

JAVASCRIPT
console.log("start");
setTimeout(() => console.log("macrotask"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("end");
// start, end, microtask, macrotask
Microtasks run before any pending macrotask.

async/await is sugar on top of promises, but it hides a performance trap: each await inside a loop serializes the operations. If the requests are independent, start them all first, then wait for the batch with Promise.all. For three 100 ms requests, the sequential version costs 300 ms, the parallel version 100 ms.

JAVASCRIPT
const fetchUser = (id) => new Promise((res) => {
  setTimeout(() => res({ id }), 100);
});
async function loadSequential(ids) {
  const users = [];
  for (const id of ids) {
    users.push(await fetchUser(id)); // ~100 ms each
  }
  return users;
}
async function loadParallel(ids) {
  return Promise.all(ids.map(fetchUser)); // ~100 ms total
}
Same result, latency divided by the number of calls.

Pick the right combinator for the failure contract you need: Promise.all rejects on the first failure (all-or-nothing), allSettled waits for everyone and describes each outcome, race takes the first settlement (success or failure, useful for a timeout), any takes the first success and only rejects if everything fails. For a dashboard that must show partial results, allSettled is almost always the right choice.

JAVASCRIPT
const tasks = [
  Promise.resolve("profile loaded"),
  Promise.reject(new Error("network unavailable")),
];
const results = await Promise.allSettled(tasks);
for (const r of results) {
  if (r.status === "fulfilled") {
    console.log("success:", r.value);
  } else {
    console.error("failure:", r.reason.message);
  }
}
Every outcome is described; no failure hides the others.

Knowledge check

Make sure you remember the key points of this lesson.

  1. In what order do a synchronous console.log, a Promise.resolve().then callback, and a setTimeout(fn, 0) callback run?
    • synchronous, setTimeout, then
    • synchronous, then, setTimeout
    • then, synchronous, setTimeout
  2. You fire ten independent requests. Which approach minimizes total latency?
    • One await per request inside a for...of loop
    • Create all the promises, then await Promise.all
    • Chain the .then calls one after another
    • Use setTimeout to space out the calls
  3. Why does try { doAsync(); } catch (e) {} without await fail to catch the rejection from doAsync?
    • Because catch does not work inside async functions
    • Because the rejection happens after the try block has exited, since the promise is never awaited
    • Because promises can only be caught with .catch