HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
6 Topics
Chapter•100 Minutes

Asynchronous JavaScript and the Event Loop

Develop a deep understanding of promises, microtasks, macrotasks, timers, async/await, event-loop ordering, and concurrency.

Synchronous vs Asynchronous Work

Synchronous code executes as part of the current call stack. Asynchronous APIs allow work to be initiated without forcing the current JavaScript execution to wait for completion. The actual mechanism depends on the host environment.

Asynchrony does not mean JavaScript suddenly executes multiple JavaScript statements simultaneously on the same agent. Instead, completed asynchronous operations schedule callbacks or promise reactions to be processed later.

Asynchronous Scheduling
Key Takeaways
  • Asynchronous APIs prevent the current operation from waiting unnecessarily.
  • Completion callbacks run later.
  • Asynchrony and parallel JavaScript execution are different concepts.
Knowledge Check

Does an asynchronous API necessarily mean JavaScript executes two pieces of JS simultaneously on one event-loop agent?

Promises as State Machines

A Promise represents the eventual result of an asynchronous operation. It starts pending and transitions to either fulfilled or rejected. Once settled, its state does not change again.

Calling then, catch, or finally creates promise reactions that are scheduled asynchronously. A promise does not itself perform background work; it represents and coordinates eventual completion.

Promise States
Key Takeaways
  • A Promise can be pending, fulfilled, or rejected.
  • Settled promises cannot transition again.
  • Promises represent eventual outcomes and chain asynchronous operations.
Knowledge Check

Which transition is valid for a Promise?

Microtasks vs Tasks

Promise reactions and queueMicrotask() use the microtask queue. Timers and many host callbacks are scheduled as tasks. After the current JavaScript job completes, the runtime processes available microtasks before moving on to another task.

This is why a resolved Promise can run before a zero-delay timer.

Promise vs setTimeout
console.log('A');

setTimeout(() => {
  console.log('B');
}, 0);

Promise.resolve().then(() => {
  console.log('C');
});

console.log('D');
💡 The synchronous logs A and D execute first. The Promise reaction is processed as a microtask before the timer task, producing C before B.
Key Takeaways
  • Synchronous code runs before queued asynchronous callbacks.
  • Promise reactions are microtasks.
  • Microtasks are processed before the runtime proceeds to another task.
Knowledge Check

What is the output order of the example?

async/await Is Promise Syntax

An async function always returns a Promise. await pauses the execution of that async function until the awaited Promise settles, but it does not block the JavaScript thread. The rest of the async function becomes a continuation that resumes later.

This distinction is critical: await makes asynchronous code easier to read without turning asynchronous operations into synchronous blocking operations.

Async Function
async function loadUser() {
  const response = await fetch('/api/user');
  const user = await response.json();
  return user;
}

loadUser().then(console.log);
💡 The async function returns a Promise. Each await suspends that function's continuation until the awaited operation settles.
Key Takeaways
  • Every async function returns a Promise.
  • await pauses the async function, not the entire JavaScript runtime.
  • Async/await is built around Promise semantics.
Knowledge Check

What does an async function always return?

Promise.all, allSettled, race, and any

Promise.all waits for every input to fulfill and rejects as soon as one rejects. Promise.allSettled waits for every input and reports each outcome. Promise.race settles when the first input settles. Promise.any fulfills when the first input fulfills and rejects only when every input rejects.

Choosing the correct combinator is about business semantics, not just syntax.

Parallel Independent Requests
const [user, posts] = await Promise.all([
  fetch('/api/user').then(r => r.json()),
  fetch('/api/posts').then(r => r.json())
]);

console.log(user, posts);
💡 Both requests begin without waiting for each other. Promise.all waits for both successful results.
Key Takeaways
  • Promise.all is appropriate when every operation must succeed.
  • Promise.allSettled is useful when every result matters regardless of success.
  • Promise.any is useful when any successful source is sufficient.
Knowledge Check

Which Promise combinator waits for all inputs regardless of whether they fulfill or reject?

A Deeper Event Loop Mental Model

The event loop should be understood as coordination between execution jobs and host scheduling mechanisms rather than as one universal queue. Browsers and Node.js have different host-level details, but the common model is that JavaScript executes jobs, asynchronous operations complete in the host, and continuations become eligible for later execution.

A long-running synchronous task prevents queued work from running. Likewise, repeatedly scheduling microtasks can delay tasks because the runtime must process the microtask queue before moving to another task.

Microtask Starvation
let count = 0;

function loop() {
  count++;
  if (count < 100000) {
    queueMicrotask(loop);
  }
}

setTimeout(() => console.log('timer'), 0);
loop();
💡 The timer cannot execute until the currently scheduled microtasks have been processed. Excessive microtask scheduling can therefore delay ordinary tasks.
Key Takeaways
  • The event loop coordinates JavaScript jobs with host scheduling.
  • Long synchronous work blocks subsequent JavaScript execution.
  • Excessive microtasks can delay task execution.
Knowledge Check

Why can a long chain of microtasks delay a timer?