Level 4, Real Data & Architecture, Chapter 16 of 24
Asynchronous JavaScript — Promises & Async/Await
Why this matters
JavaScript runs on a single thread, meaning it can only do one thing at a time, yet web pages constantly wait on things that take time: network requests, timers, animations, file reads. If JavaScript simply paused and did nothing while waiting, every page would freeze whenever it needed to fetch data. Instead, JavaScript uses an asynchronous model that lets it keep responding to clicks and scrolls while slow operations happen in the background.
You've already brushed up against this with setTimeout and the .then() chains in the previous chapter. Now you'll understand the mechanism underneath, Promises, and learn async/await, the modern syntax that makes asynchronous code read almost like ordinary step-by-step code. This is one of the most important mental models in all of JavaScript, and it will make every future API, animation, or timer-based feature much easier to reason about.
The lesson
The problem synchronous code can't solve
Imagine calling a function to fetch data from a server and having your program freeze until the response arrives. Every scroll, click, and animation would stop, sometimes for seconds. Browsers avoid this by handing off slow tasks, like network requests and timers, to the browser itself, which notifies your JavaScript when they finish, rather than blocking everything in the meantime. This is why setTimeout(fn, 1000) doesn't pause your script for a second, it schedules fn to run later and immediately moves on to the next line. Understanding that JavaScript keeps running while these tasks are pending is the key to understanding everything else in this chapter.
Promises represent a future value
A Promise is an object representing a value that isn't available yet but will be, eventually, either successfully (resolved) or unsuccessfully (rejected). You've already used Promises implicitly through fetch(). You can also create your own with new Promise((resolve, reject) => { ... }), calling resolve(value) when the work succeeds and reject(error) when it fails. Once you have a Promise, .then(onSuccess) runs when it resolves, and .catch(onError) runs when it rejects. Promises can also be chained, since .then() itself returns a new Promise, which is what lets you write fetch(url).then(...).then(...).
1function wait(ms) {2 return new Promise((resolve) => {3 setTimeout(resolve, ms);4 });5}67wait(1000).then(() => console.log("One second has passed"));async/await: cleaner syntax for the same thing
Chaining many .then() calls together can get hard to read, especially once you add error handling and conditional logic. async/await solves this by letting you write asynchronous code that looks synchronous. Marking a function async means it always returns a Promise, and lets you use the await keyword inside it. await somePromise pauses execution of that function (without freezing the rest of the page) until the promise settles, then gives you the resolved value directly, as if it had returned normally. Errors from a rejected promise show up as regular thrown exceptions, which means you can catch them with a familiar try/catch block instead of chaining .catch().
1async function loadUsers() {2 try {3 const response = await fetch("https://jsonplaceholder.typicode.com/users");4 if (!response.ok) {5 throw new Error(`Request failed with status ${response.status}`);6 }7 const users = await response.json();8 console.log(users.map((u) => u.name));9 } catch (error) {10 console.error("Could not load users:", error.message);11 }12}1314loadUsers();A common early mistake is forgetting that await only works inside a function marked async, and that calling an async function doesn't block the code that called it, since the async function itself returns a Promise immediately. If you need to run several independent asynchronous operations at the same time rather than one after another, Promise.all([promise1, promise2]) lets you await multiple promises in parallel and get their results back together, once all of them finish. This is much faster than awaiting each one in sequence when they don't depend on each other.
1async function loadDashboard() {2 const [users, posts] = await Promise.all([3 fetch("https://jsonplaceholder.typicode.com/users").then((r) => r.json()),4 fetch("https://jsonplaceholder.typicode.com/posts").then((r) => r.json()),5 ]);6 console.log(users.length, posts.length);7}- A Promise is either pending, resolved (fulfilled), or rejected
.then()handles a resolved value,.catch()handles a rejectionasyncfunctions always return a Promiseawaitpauses inside an async function until a Promise settles- Wrap awaited code in try/catch to handle errors
Promise.all([...])runs multiple promises concurrently
Try it yourself
Practice async/await with a simulated slow operation. Click the button and watch the status update while the page stays responsive.
Chapter boss project
Build a Sequenced Loader with Error Recovery
Build a small app that loads three pieces of data in sequence, using simulated async functions: user profile, then that user's posts, then the comment count for the first post. Each step depends on the previous one's result, so you'll chain awaits rather than using Promise.all.
Show the current step's status on the page as it happens, and design one of your simulated steps to randomly fail about one time in four. When it fails, catch the error and show a retry button instead of leaving the app stuck.
This mirrors a very common real pattern: a checkout flow, a multi-step signup, or any process where later steps genuinely need results from earlier ones, and where you need to handle failures gracefully instead of assuming the network always works.
- Write each simulated step as its own async function returning a Promise that resolves after a short delay, and have one of them randomly reject using Math.random().
- Structure your main async function with a single try/catch wrapping all the awaited steps.
- Update a status element's text right before each await so the user sees progress as it happens.
- For the retry button, simply call your main async function again when it's clicked.
Level-up checklist
Tick these off once each one is true for you.
