Section 4 · JavaScript in the Browser
Async JavaScript: Promises, async/await and fetch
22 min4 min readCode playground
Lesson at a glance
Explain why JavaScript waits without freezing, read and write promise-based code with then and async/await, call fetch with proper error handling, and predict output order using the event loop.
- 1Callbacks: “call me when it is done”
- 2Promises: a value that arrives later
- 3async and await: promises that read like normal code
- 4fetch, for real
- 5The event loop
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Pocket would be more useful if it could show your spending in euros too, which means asking a server for today’s exchange rates. That request might take 50 milliseconds or five seconds. If JavaScript simply stopped and waited, the whole page would freeze: no scrolling, no typing, no clicks. Since your code shares one thread with the page itself, it needs a way to start slow work, carry on, and pick up the result later. That is what asynchronous code is.
Callbacks: “call me when it is done”
The oldest form of “later” is a callback. setTimeout asks the browser to call a function after a delay:
console.log("Requesting rates…");
setTimeout(() => {
console.log("Rates arrived");
}, 100);
console.log("Still responsive");“Still responsive” prints before “Rates arrived”. setTimeout does not pause anything; it hands the function to the browser and returns immediately. Callbacks work, but when step two needs step one’s result and step three needs step two’s, they nest deeper and deeper, and error handling has to be repeated at every level. Promises were added to fix that.
Promises: a value that arrives later
A promise is an object that stands for a result that is not ready yet. It starts pending, and settles exactly once: fulfilled with a value, or rejected with an error. You attach what should happen next with .then (on success) and .catch (on failure).
This course’s sandbox has no network, so the examples use fakeFetch, a stand-in that behaves like the real fetch: it returns a promise of a response object with ok, status and a json() method.
function fakeFetch(url) {
return new Promise((resolve) => {
setTimeout(() => {
if (url === "/api/rates") {
resolve({ ok: true, status: 200, json: async () => ({ EUR: 0.86, GBP: 0.75 }) });
} else {
resolve({ ok: false, status: 404, json: async () => ({ error: "Not found" }) });
}
}, 50);
});
}
fakeFetch("/api/rates")
.then((response) => response.json())
.then((rates) => console.log("EUR rate:", rates.EUR))
.catch((error) => console.log("Failed:", error.message));Each .then returns a new promise, so steps chain in a flat line instead of nesting, and one .catch at the end handles a failure from any step. You will rarely create promises with new Promise yourself; libraries and browser APIs hand them to you, and your job is to consume them.
async and await: promises that read like normal code
async/await is a more readable way to write the same thing. Inside a function marked async, await promise pauses that function until the promise settles, then gives you its value. Errors become ordinary exceptions you catch with try/catch:
function fakeFetch(url) {
return new Promise((resolve) => {
setTimeout(() => {
const ok = url === "/api/rates";
const body = ok ? { EUR: 0.86 } : { error: "Not found" };
resolve({ ok, status: ok ? 200 : 404, json: async () => body });
}, 50);
});
}
async function getJson(url) {
const response = await fakeFetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status} for ${url}`);
}
return response.json();
}
async function showRates() {
try {
const rates = await getJson("/api/rates");
console.log("EUR:", rates.EUR);
await getJson("/api/missing");
} catch (error) {
console.log("Could not load:", error.message);
}
}
showRates();Notice what await does not do: it does not freeze the page. While showRates waits, the browser keeps handling clicks and drawing frames. Only this one function is paused.
An async function always returns a promise, even when you return a plain value. So whoever calls it must await it too, or use .then.
fetch, for real
In a real page, you replace fakeFetch with the browser’s fetch, and the shape of the code stays the same:
async function getJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status} for ${url}`);
}
return response.json();
}The response.ok check is essential, and many tutorials leave it out. fetch only rejects when the request could not happen at all, for example when the user is offline. A 404 or a 500 is a perfectly good HTTP response, so the promise resolves, with ok set to false. Without the check, your code would try to read rates out of an error page.
When two requests do not depend on each other, start both and wait once with Promise.all (inside an async function, like every await), which fulfils with an array of results or rejects as soon as either fails:
const [rates, categories] = await Promise.all([
getJson("/api/rates"),
getJson("/api/categories"),
]);The event loop
How can one thread do all this? The engine runs your code on a call stack, one function at a time. Slow things, such as timers and network requests, are handled by the browser outside the stack. When one finishes, its callback is put in a queue. Whenever the stack is empty, the event loop takes the next callback from a queue and runs it.
There are two queues, and the order between them explains many puzzles. Promise callbacks go into the microtask queue; timers and events go into the task queue. Each time the stack empties, the loop runs every waiting microtask first, then one task, then checks microtasks again:
console.log("1: script");
setTimeout(() => console.log("4: timeout task"), 0);
Promise.resolve().then(() => console.log("3: promise microtask"));
console.log("2: script");A timeout of 0 ms does not mean “now”; it means “as a task, once the stack is clear and the microtasks are done”. The same model explains why a long loop freezes the page: while your code occupies the stack, no click handler, timer or redraw can run.
Section 4 is complete: Pocket lives on a page, reacts to clicks, takes input, saves data and can talk to a server. Section 5 turns this growing pile of code into a well-organised program.
Key takeaways
- JavaScript runs one thing at a time; slow work such as network requests and timers happens outside your code, which is called back when it finishes.
- A promise is an object standing for a value that will arrive later; it ends up fulfilled with a value or rejected with an error.
awaitpauses only the surroundingasyncfunction until a promise settles; wrap awaits intry/catchto handle rejections.fetchrejects only when the network fails; for HTTP errors such as 404 it resolves withresponse.okset to false, so always check it.- After each piece of synchronous code, the event loop runs all queued promise callbacks before the next timer or event.