BeginnerAsynchronous basics and data fetching~40 min

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:

JavaScriptAn async function, awaiting a promise
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:

JavaScriptfetch, checking status, and parsing JSON
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:

Three states beyond the happy path, and what to show for each
StateWhenWhat to show
LoadingRequest started, no response yetA visible loading indicator
EmptyRequest succeeded, zero resultsAn honest "nothing here" message, not a blank screen
ErrorRequest 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

What does this log, and in what order? console.log('start'); setTimeout(() => console.log('timeout'), 0); console.log('end');
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.

A fetch request to a real URL returns a 404 response. Does the promise returned by fetch() reject?
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.

  1. Hint 1

    Everything before the first await inside an async function runs synchronously, immediately, when the function is called.

  2. Hint 2

    The code after await (here, console.log("B")) is scheduled to resume only after the current synchronous code finishes.

  3. 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
B

Practice: 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.

  1. Hint 1

    Wrap the fetch and .json() calls so a network failure is caught, not just a bad status.

  2. Hint 2

    Check response.ok before calling response.json() — parsing a non-JSON error page as JSON would throw a confusing, unrelated error.

  3. 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/await lets you write that flow as readable, sequential-looking code.
  • await only pauses the async function it’s inside, not the rest of the program.
  • fetch’s promise rejects only on a network-level failure — always check response.ok or response.status to detect an HTTP error status.
  • A resilient UI represents loading, empty, and error states explicitly, not just the success case.
  • Promise.all runs independent requests concurrently, but rejects as a whole if any one of them rejects.

Related reference