CHAPTER 03 · INTERMEDIATE4 min read

Promises, the Event Loop, and Async/Await

Demystify asynchronous execution in JavaScript, understand microtask queues, and master async/await flow control.

ES2024 / Node.js 20+Free to readModule 2 of 3
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Explain the interplay between the call stack, Web APIs, task queue, and microtask queue.
  • Construct and resolve Promises while implementing robust error handling with catch and finally.
  • Write clean, synchronous-looking asynchronous flows with async/await and structured try/catch blocks.

01The Event Loop and Non-Blocking Concurrency

JavaScript is single-threaded: it has a single call stack and can only execute one chunk of code at a time. To prevent long-running tasks like network requests or file access from freezing the entire program, JavaScript offloads these operations to host environment APIs (such as browser Web APIs or Node.js libuv threads). When the background operation finishes, its completion callback is enqueued to run when the main stack clears.

Crucially, JavaScript distinguishes between the task queue (macro-tasks like setTimeout and setInterval) and the microtask queue (Promise callbacks and queueMicrotask). The event loop always processes all pending microtasks before executing the next macro-task. Understanding this priority order enables you to anticipate exact code execution sequences under high concurrency.

In production engineering environments, reliability and predictability are paramount. Every line of code must account for edge cases, resource boundaries, and unexpected inputs. By establishing strict invariants and separating pure business logic from side effects, developers construct maintainable software architectures that scale smoothly across teams and withstand rigorous continuous integration testing.

example.js⚡JavaScript
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

02From Callback Hell to Promises

Early asynchronous JavaScript depended heavily on nested callbacks. When multiple asynchronous steps had to be coordinated sequentially, code formed deeply nested pyramid patterns commonly referred to as callback hell. Error handling in this model was notoriously brittle because each callback required manual error propagation.

Promises transformed asynchronous programming by providing a standard state machine for deferred values. A Promise exists in one of three states: pending, fulfilled, or rejected. Promises chain cleanly using .then() and .catch(), allowing synchronous-style pipeline composition with centralized error capturing.

example.js⚡JavaScript
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

03Writing Expressive Code with Async and Await

Introduced in ECMAScript 2017, the async and await keywords provide elegant syntactic sugar over Promises. Marking a function with async guarantees that it automatically returns a Promise. Inside an async function, you can pause execution until a Promise resolves using the await keyword, without blocking the browser thread or Node.js event loop.

With async/await, error handling returns to familiar try/catch/finally structures. This makes asynchronous code significantly easier to read, reason about, and debug, while completely preserving the non-blocking power of the underlying runtime.

example.js⚡JavaScript
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

04Coordinating Multiple Concurrent Requests

A common anti-pattern when using await is chaining independent requests sequentially, which artificially inflates total network latency. If three requests do not depend on each other's data, they should be fired concurrently and gathered when ready.

JavaScript provides powerful combinators like Promise.all (which rejects immediately if any promise fails) and Promise.allSettled (which waits for all promises to conclude regardless of outcome). Choosing the correct combinator ensures optimal throughput and graceful degradation in client applications.

example.js⚡JavaScript
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
LET IT CLICK

The ideas worth keeping.

  • JavaScript single-threading relies on host APIs and the event loop for non-blocking I/O.
  • Microtasks (Promises) always run before macro-tasks (timers and I/O callbacks).
  • Use Promise.allSettled when coordinating multiple requests that can succeed or fail independently.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Delay Utility

Write a utility function sleep(ms) that returns a Promise resolving after the specified milliseconds.

your_turn_1.js⚡JavaScript
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.jsJavaScript
function sleep(ms) {  return new Promise((resolve) => setTimeout(resolve, ms));}

Wrapping setTimeout inside a Promise constructor yields a composable delay tool that can be paused using await sleep(1000).

02

Safe JSON Parser

Write an async function safeFetchJson(url) that fetches a URL and parses its JSON, returning null if the network or parsing fails.

your_turn_2.js⚡JavaScript
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.jsJavaScript
async function safeFetchJson(url) {  try {    const res = await fetch(url);    if (!res.ok) return null;    return await res.json();  } catch {    return null;  }}

Wrapping both the network fetch and JSON parsing steps inside try/catch prevents uncaught rejections when handling invalid responses.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01What happens when an unhandled error is thrown inside an async function?
02Which task queue takes priority when the call stack becomes empty?
03What is the key advantage of Promise.allSettled over Promise.all?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Delay Utility

Write a utility function sleep(ms) that returns a Promise resolving after the specified milliseconds.

EXPECTED OUTPUT
[Process exited with code 0 - No output]
challenge.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this chapter, when you want a second angle or the full details.

One chapter closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock chapter completion.