JavaScript Async / Await Complete

Asynchronous JavaScript

JavaScript runs on a single thread yet handles thousands of concurrent network calls. Master callbacks, Promises, async/await, and the event loop that makes it all work.

ES2023 Concurrency 7 Topics Interview Key
01

Callbacks & Callback Hell

The original async pattern: pass a function to run later, when the result is ready. It works, but nesting several of them produces the "pyramid of doom" that Promises and async/await were built to fix.

JavaScript — Callback Hell
getUser(id, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      console.log(comments);     // three levels deep, and error handling repeats at each level
    }, onError);
  }, onError);
}, onError);
Every callback needs its own error path, and the nesting only grows with each dependent step. This exact shape is what Promises were designed to flatten.
02

Promises

A Promise represents a value that isn't ready yet. It's always in one of three states: pending, fulfilled, or rejected — and once settled, it never changes state again.

JavaScript — Creating & Chaining Promises
function delay(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve("done"), ms);
  });
}

delay(1000)
  .then(result => { console.log(result); return delay(500); })   // chain flattens nesting
  .then(() => console.log("second delay done"))
  .catch(err => console.error(err))                        // one handler, not one per step
  .finally(() => console.log("cleanup, runs either way"));
Pending
Initial state, outcome not yet known.
Fulfilled
Resolved with a value — triggers .then().
Rejected
Failed with a reason — triggers .catch().
Settled
Fulfilled or rejected — permanent, cannot change again.
03

async / await

async/await is syntax sugar over Promises — it lets asynchronous code read top-to-bottom like synchronous code, without giving up any of the non-blocking behavior underneath.

JavaScript — Same Logic, Readable Order
async function loadFeed(userId) {
  const user = await getUser(userId);        // pauses here, doesn't block the thread
  const posts = await getPosts(user.id);
  const comments = await getComments(posts[0].id);
  return comments;             // same three calls as callback hell, flat and readable
}

// calling an async function always returns a Promise
loadFeed(1).then(comments => console.log(comments));
await is only legal inside an async function (or at the top level of a module). It suspends that function only — the rest of the program keeps running.
04

Promise Combinators

JavaScript — all, allSettled, race, any
const urls = ["/a", "/b", "/c"];

// all — fails fast: one rejection rejects everything
const results = await Promise.all(urls.map(fetch));

// allSettled — always resolves, gives you each outcome
const outcomes = await Promise.allSettled(urls.map(fetch));
// [{status:"fulfilled", value:...}, {status:"rejected", reason:...}, ...]

// race — settles as soon as the FIRST one settles (win or lose)
const fastest = await Promise.race([fetch("/a"), delay(3000)]);

// any — settles on the first FULFILLED result, ignores rejections
const firstOk = await Promise.any(urls.map(fetch));
MethodResolves whenRejects when
Promise.allEvery promise fulfillsAny single one rejects
Promise.allSettledAll promises settleNever — always resolves
Promise.raceFirst one settles (either way)If that first one rejects
Promise.anyFirst one fulfillsAll of them reject
05

The Event Loop — Microtasks vs Macrotasks

JavaScript is single-threaded, so it needs a scheduler to interleave "waiting" work with running code — that's the event loop. It runs all synchronous code first, then drains the microtask queue completely, then processes exactly one macrotask, and repeats.

JavaScript — Guess the Order
console.log("1: sync");

setTimeout(() => console.log("4: macrotask (setTimeout)"), 0);

Promise.resolve().then(() => console.log("3: microtask (promise)"));

console.log("2: sync");

// Output order: 1, 2, 3, 4
// All sync code runs first, THEN all microtasks, THEN the next macrotask.

Microtasks always cut in line

Promise callbacks (microtasks) always run before the next setTimeout/setInterval/I/O callback (macrotasks), even if the timeout was scheduled first with a delay of 0.

06

Error Handling

JavaScript — try/catch with await
async function safeLoad(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error("Load failed:", err.message);
    return null;              // caller gets a safe fallback instead of a crash
  }
}
A rejected Promise with no .catch() or surrounding try/catch fires an unhandledrejection event and, in Node, can crash the process.
07

Common Pitfalls

await in a loop = sequential

for (const u of urls) await fetch(u) runs one at a time. Build an array of promises and Promise.all them for real concurrency.

Forgetting to return inside .then()

If a .then() callback doesn't return the next promise, the chain doesn't wait for it — it moves on immediately with the wrong value.

Mixing async with forEach

arr.forEach(async x => await work(x)) doesn't wait for anything — forEach ignores the returned promises. Use a for...of loop or Promise.all(arr.map(...)).

async isn't parallel, it's concurrent

Still single-threaded. For real CPU parallelism, JavaScript needs Web Workers (browser) or worker_threads (Node) — async only overlaps I/O waits.

Quick Quiz

1. Calling an async function directly returns…
2. Which settles as soon as any promise rejects?
3. Microtasks (Promises) run relative to macrotasks (setTimeout) how?
4. arr.forEach(async x => await work(x)) — does forEach wait?
5. A Promise, once settled…