Asynchronous JavaScript: timers, promises and fetch
Lesson 8 of 8
8Asynchronous JavaScript: timers, promises and fetch
The big pictureEvery real app talks to a server: it fetches the product list, sends the order, updates the balance. The network is slow and unreliable. If the page froze while waiting for the reply, you could not even scroll. This lesson teaches you to ask for something now and act when it arrives, and it is the bridge between the interface you built and the server you will build in the next track.
What the picture shows: The request goes out to the network, and the page stays alive and responsive. When the reply arrives, the code carries on after await: it reads the data and draws it, or shows an error if it failed.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
Imagine ordering coffee in a busy café. The synchronous way: you stand at the till and the cashier serves no one else until your coffee is ready, while the whole queue waits. The asynchronous way: they take your order and hand you a buzzer (that is the Promise, a "promise" of coffee later), you sit down and carry on talking, and the cashier serves others. When the buzzer goes off (the promise is fulfilled) you collect your coffee. Sometimes it buzzes to say "we are out of milk" (the promise is rejected), and you deal with it.
JavaScript in the browser is single-threaded with an event loop: the call stack runs synchronous code until it is empty, then tasks are taken from queues: the microtask queue (promise reactions and queueMicrotask) is drained completely before each macrotask (timers, UI events, network replies). A Promise is an object with three states, pending then fulfilled or rejected, and a .then/.catch/.finally chain. async makes a function always return a Promise, and await suspends the function (not the thread) until the promise settles, with a rejection becoming an exception caught by try/catch. fetch returns a Promise that resolves to a Response even for 404 and 500, so check res.ok; it rejects only on network failure, CORS, or cancellation with AbortController. Promise.all runs in parallel and fails if any one fails, and Promise.allSettled waits for all of them.
Before You Start
🧰Functions, arrays and objects
You know arrow functions, callbacks and JSON.
🖱️JavaScript and the DOM
You change the page and listen for events.
📨HTTP
You know requests, responses and status codes like 200 and 404.
The Core Idea
1Why "asynchronous"? The event loop
JavaScript runs one line at a time. If every wait stopped everything, the page would freeze for two seconds on every server request. The fix: slow jobs are handed to the browser (or Node) to watch, and the code carries on. When they finish they join a queue, and the event loop runs them once the current code is done.
setTimeout(fn, ms) is the simplest example: "call this function after so many milliseconds". Run the example and predict the order before you see the output:
console.log("1. take the order");
setTimeout(() => {
console.log("4. coffee ready (after 300 ms)");
}, 300);
setTimeout(() => {
console.log("3. receipt printed (after 0 ms, but still waits its turn)");
}, 0);
console.log("2. serve the next customer");Line 3 came after 2 even though its timer was zero! Because any timer waits until all current code finishes, then the event loop takes it from the queue. This one idea explains half of JavaScript's puzzles.
2Promise: a promise of a value later
A Promise is an object standing for a result that has not arrived yet. It has three states: pending, then either fulfilled with a value, or rejected with an error. You say what to do when it is fulfilled with .then(fn), when rejected with .catch(fn), and in both cases with .finally(fn).
function brew(drink) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (drink === "tea") reject(new Error("we only serve coffee"));
else resolve(`your ${drink} is ready`);
}, 200);
});
}
console.log("ordered, waiting...");
brew("latte")
.then((msg) => console.log("OK:", msg))
.catch((err) => console.log("FAILED:", err.message))
.finally(() => console.log("buzzer returned"));
brew("tea")
.then((msg) => console.log("OK:", msg))
.catch((err) => console.log("FAILED:", err.message));new Promise yourself very often. Most of the time promises come ready-made from functions like fetch, and your job is to wait for them and handle the result. We wrote one here so you can see inside it.3async and await: asynchronous code that reads like normal code
Chains of .then get hard to read as they grow. The modern fix: write async before the function, then await before any Promise: "wait here until the reply arrives, then carry on". The function pauses, but the page does not. Errors are handled with try { ... } catch (err) { ... } like any normal code.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// a pretend server: answers after a short delay
async function getMenu() {
await wait(150);
return [{ name: "latte", price: 14 }, { name: "v60", price: 18 }];
}
async function getStock(name) {
await wait(100);
if (name === "v60") throw new Error("v60 beans are out of stock");
return 12;
}
async function showMenu() {
try {
const menu = await getMenu();
console.log("menu:", menu.map((d) => d.name).join(", "));
for (const drink of menu) {
const stock = await getStock(drink.name);
console.log(drink.name, "stock:", stock);
}
} catch (err) {
console.log("could not finish:", err.message);
} finally {
console.log("done loading");
}
}
showMenu();
console.log("the page is still responsive while we wait");await. const menu = getMenu() without await gives you a Promise, not the array, so it says menu.map is not a function. If you ever see [object Promise] on screen, that is usually why.4fetch: ask the server for data
fetch(url) sends an HTTP request and returns a Promise of a Response. Then await res.json() reads the body and turns it from JSON into an object. Careful: fetch treats 404 and 500 as "success" because the server did reply, so check res.ok yourself. To send data use method: "POST" and a JSON body.
// GET: read data
async function loadMenu() {
const res = await fetch("https://api.example.com/menu");
if (!res.ok) throw new Error(`server said ${res.status}`);
return res.json();
}
// POST: send data
async function placeOrder(order) {
const res = await fetch("https://api.example.com/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(order),
});
if (!res.ok) throw new Error(`order failed: ${res.status}`);
return res.json(); // e.g. { id: 42, status: "received" }
}In the next playground we fetch real data with fetch inside the browser. So it works offline, it requests a "data URL" containing JSON instead of a server. In your project you put the real API link in its place and the code stays the same:
A real fetch in the browser
Press the button. Then change URL to "https://invalid.invalid/menu" and press again to watch the error branch run.
5Interface states: loading, error, empty and success
Every screen that fetches data has four states you must design, not just success: loading (show an indicator and disable the button so it is not pressed twice), error (an understandable message and a "try again" button), empty (no results: say so clearly), and success. A user staring at a blank screen cannot tell whether to wait or leave.
Try all four states
Search normally, then tick "fail", then "empty". Notice the button is disabled while loading. The texts here are fixed in the code, so innerHTML is safe; with text from a user, use textContent.
6Several requests at once: Promise.all
If you need three independent things (the menu, the offers, the branches), awaiting them one after another adds up their times. Promise.all([...]) starts them all together and waits for them, so the total time is just the slowest one. If one fails they all fail, so when you want each result no matter what, use Promise.allSettled.
const wait = (ms, value) => new Promise((r) => setTimeout(() => r(value), ms));
const getMenu = () => wait(300, ["latte", "mocha"]);
const getOffers = () => wait(200, ["2 for 1 on Fridays"]);
const getBranches = () => wait(200, ["Olaya", "Malqa"]);
async function oneByOne() {
const t = Date.now();
await getMenu(); await getOffers(); await getBranches();
return Date.now() - t;
}
async function allTogether() {
const t = Date.now();
const [menu, offers, branches] = await Promise.all([getMenu(), getOffers(), getBranches()]);
console.log(menu.length, "drinks,", offers.length, "offer,", branches.length, "branches");
return Date.now() - t;
}
(async () => {
const slow = await oneByOne();
const fast = await allTogether();
console.log("one by one ~", Math.round(slow / 100) * 100, "ms");
console.log("together ~", Math.round(fast / 100) * 100, "ms");
})();Worked example
Problem: a coffee-order page that loads the menu; the user picks a drink and presses "Order" to send it, with the button disabled while sending and a success message with the order number, or an error message. Solution: one async function per request, and one state the interface is drawn from. The server here is simulated with wait so the example works offline:
A complete asynchronous order page
Order a latte, then a V60, to see success and failure. In the Express lesson you write the real server that replaces api here.
Myth vs Reality
| Myth | Reality |
|---|---|
| async/await makes JavaScript multi-threaded. | It is still one thread. await only suspends the function and lets the event loop run other work. |
| fetch throws if the server replies 404. | It counts it as a successful reply. Check res.ok yourself and throw the error. |
| setTimeout(fn, 0) runs immediately. | It waits for all current code to finish, then takes its turn in the queue. |
Where you meet it at work
🛒Loading products and orders
Every list you see in a shop came from a fetch.
💬Notifications and chat
Waiting for new messages without freezing the page.
🧑🏫Darb's smart tutor
It sends your question to the server and awaits the reply.
Test your understanding
What order prints: console.log("A"); setTimeout(() => console.log("B"), 0); console.log("C");?
The timer waits for the current code to finish even at zero.
You wrote const data = fetch(url) without await. What is in data?
fetch returns a Promise at once; await is what waits for its value.
The server replied 500. What does fetch do?
The server did reply, so the promise resolves, and checking res.ok is your job.
Where do you catch an await error?
A rejected promise becomes an exception at await, caught by catch.
Three independent requests take a second each. How long with Promise.all?
They start together, so the total is the slowest one's time.
Quick review cards
Check Your Understanding
Why does the page not freeze during fetch?
Because the browser watches the request, the code carries on, and when the reply arrives the event loop queues it.
What is the difference between a rejected promise and res.ok = false?
Rejection means the request never got through or got no reply (network); res.ok = false means the server replied with an error code like 404.
Why disable the button while sending?
So the user does not press it twice and send two orders, and so they know something is happening.
When do you use Promise.all instead of successive awaits?
When the requests are independent and none needs another's result.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs1
Done with this lesson?
Sign in to save your progress.