Asynchronous Basics and Data Fetching
Why waiting for something needs a different execution model, how promises and async/await represent an eventual outcome, and fetching data with resilient loading/empty/error states.
By the end of this lesson, you can
- Explain why JavaScript can't just "pause" while waiting for something like a network response
- Read and write async/await code, including handling a rejected promise with try/catch
- Use fetch to request data, check the response status, and parse JSON from it
- Represent loading, empty, and error states explicitly, rather than only handling the success case
Why it matters
A network request can take anywhere from milliseconds to several seconds. If JavaScript simply “paused” while waiting, the entire page would freeze — no scrolling, no clicking, nothing — for as long as the request took. Asynchronous code is how JavaScript keeps running while something slow happens in the background, and picks back up once it’s ready.
Why waiting needs a different model
JavaScript runs on a single thread — one thing at a time, in order. A function that could “block”
while waiting for a network response would freeze everything else on the page for however long that
takes. Instead, an operation like fetch starts the request, returns immediately with a placeholder
for the eventual result, and the rest of the program keeps running. When the response is finally
ready, JavaScript calls back into your code to hand it over.
Promises and async/await
A promise represents a value that isn’t available yet, but will be — either successfully
(fulfilled) or with an error (rejected). async/await is syntax for working with promises that
reads like ordinary sequential code:
async function getGreeting() {
const name = await lookUpName(); // pauses this function (not the whole program) until resolved
return `Hello, ${name}!`;
}await only pauses the async function it’s inside — the rest of the program, including event
handlers and other code, keeps running normally while it waits.
A rejected promise is handled with try/catch, the same statement covered in Errors and
Debugging:
async function getGreeting() {
try {
const name = await lookUpName();
return `Hello, ${name}!`;
} catch (error) {
console.error('Could not look up name:', error.message);
return 'Hello, stranger!';
}
}
fetch and HTTP essentials
fetch(url) requests a URL and returns a promise for the response:
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json(); // itself returns a promise — must be awaited too
}response.json() parses the response body as JSON and itself returns a promise — a second await
is needed, easy to forget the first few times.
Loading, empty, and error states
A resilient UI represents more than just “the data arrived.” At minimum:
| State | When | What to show |
|---|---|---|
| Loading | Request started, no response yet | A visible loading indicator |
| Empty | Request succeeded, zero results | An honest "nothing here" message, not a blank screen |
| Error | Request failed (network or bad status) | A clear error message, not a silent failure |
async function loadResults(query) {
setState('loading');
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
if (!response.ok) throw new Error(`Search failed: ${response.status}`);
const results = await response.json();
setState(results.length === 0 ? 'empty' : 'success', results);
} catch (error) {
setState('error', error.message);
}
}
Running requests in parallel
Awaiting requests one after another when they don’t depend on each other wastes time —
Promise.all runs several promises concurrently and resolves once all of them have:
async function loadDashboard() {
const [user, notifications] = await Promise.all([fetchUser(), fetchNotifications()]);
return { user, notifications };
}
Promise.all rejects as soon as any of its promises rejects — worth knowing, since it means one
failing request can short-circuit the whole group. Handling that gracefully (letting other requests
still succeed) needs Promise.allSettled, covered in the Intermediate tier’s deeper async module.
Check your understanding
Or reveal the answer without checking
Answer:'start' 'end' 'timeout'
Even with a 0ms delay, setTimeout's callback is scheduled to run after the current synchronous code finishes — 'start' and 'end' both run first, then 'timeout'. This is the same single-threaded, non-blocking model that makes await not freeze the rest of the program.
Or reveal the answer without checking
Answer:No — fetch only rejects on a network-level failure; a 404 is still a "successful" response as far as fetch is concerned
fetch()'s promise rejects only for network-level failures (offline, DNS errors, and similar) — an HTTP error status like 404 or 500 still resolves normally. Checking response.ok or response.status is the correct way to detect a failed request.
Practice: warm-up
Predict what each line logs and in what relative order, then check:
async function example() {
console.log('A');
await Promise.resolve();
console.log('B');
}
console.log('1');
example();
console.log('2');
Stuck? Reveal one hint at a time.
Hint 1
Everything before the first await inside an async function runs synchronously, immediately, when the function is called.
Hint 2
The code after await (here, console.log("B")) is scheduled to resume only after the current synchronous code finishes.
Hint 3
So the call to example() itself runs partway (logging A) before returning control back to the line that called it.
Reveal the order
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
1
A
2
BPractice: apply it
Write fetchJoke() that fetches from a joke API URL of your choice, returns the parsed JSON on
success, and throws a descriptive Error if the response status isn’t OK or the network request
itself fails.
Stuck? Reveal one hint at a time.
Hint 1
Wrap the fetch and .json() calls so a network failure is caught, not just a bad status.
Hint 2
Check response.ok before calling response.json() — parsing a non-JSON error page as JSON would throw a confusing, unrelated error.
Hint 3
Let the function's own throw carry a clear message; let the caller decide with try/catch how to handle it, per the pattern from Errors and Debugging.
Reveal a reference solution
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
async function fetchJoke() {
const response = await fetch('https://api.example.com/joke');
if (!response.ok) {
throw new Error(`Joke request failed: ${response.status}`);
}
return response.json();
}Summary
- JavaScript can’t “pause” the whole program to wait for something slow — it starts the operation, keeps running everything else, and resumes your code once the result is ready.
- A promise represents an eventual value;
async/awaitlets you write that flow as readable, sequential-looking code. awaitonly pauses theasyncfunction it’s inside, not the rest of the program.fetch’s promise rejects only on a network-level failure — always checkresponse.okorresponse.statusto detect an HTTP error status.- A resilient UI represents loading, empty, and error states explicitly, not just the success case.
Promise.allruns independent requests concurrently, but rejects as a whole if any one of them rejects.