Stage 2: Frontend Fundamentals

JavaScript · Lesson 08

Asynchronous JavaScript

fetch, Promises and async/await for loading data from APIs.

JavaScript2 min readFree lesson

Network requests take time. JavaScript runs on a single thread, so instead of freezing the page while it waits, it schedules work and continues. Promises and async/await are the modern way to write that code so it still reads top to bottom.

Learning objectives

  • Explain the difference between synchronous and asynchronous code.
  • Create and consume Promises.
  • Use async/await with fetch, and handle failures with try/catch.
  • Run requests in parallel with Promise.all, and cancel them with AbortController.

1. The problem

console.log("start");
setTimeout(() => console.log("later"), 0);
console.log("end");

// start
// end
// later

The callback does not run until the call stack is empty. This is the event loop: synchronous code finishes first, then queued work runs.

2. Promises

A Promise represents a value that will exist later. It is either pending, fulfilled or rejected.

function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

wait(500)
  .then(() => console.log("half a second later"))
  .catch((error) => console.error(error));

3. async / await

await pauses the function (not the page) until the Promise settles.

async function loadUsers() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");

    if (!response.ok) {
      throw new Error(`Request failed: ${response.status}`);
    }

    const users = await response.json();
    return users;
  } catch (error) {
    console.error("Could not load users", error);
    return [];
  }
}

Two rules that matter:

  • fetch only rejects on network failure. A 404 or 500 still resolves — check response.ok.
  • Any function that uses await must be async, and callers must handle its returned Promise.

4. Parallel requests

// slow: one after another
const user = await fetch("/api/user").then((r) => r.json());
const posts = await fetch("/api/posts").then((r) => r.json());

// fast: both start immediately
const [userRes, postsRes] = await Promise.all([
  fetch("/api/user"),
  fetch("/api/posts"),
]);

const [user2, posts2] = await Promise.all([userRes.json(), postsRes.json()]);

Promise.all rejects as soon as one request fails. Promise.allSettled waits for every request and reports each result — use it when partial data is still useful.

5. Cancelling work

If the visitor types again before the previous search returns, cancel the old request:

let controller;

async function search(query) {
  controller?.abort();
  controller = new AbortController();

  try {
    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    });
    return await response.json();
  } catch (error) {
    if (error.name === "AbortError") return null;
    throw error;
  }
}

Without this, a slow earlier request can overwrite the results of a faster later one.

6. Rendering async data

const list = document.querySelector("#results");
list.textContent = "Loading…";

const users = await loadUsers();
list.replaceChildren();

for (const user of users) {
  const item = document.createElement("li");
  item.textContent = `${user.name} — ${user.email}`;
  list.append(item);
}

Always show a loading state and an empty state. A blank screen looks like a bug.

Common mistakes

  • Forgetting await response.json() (and then wondering why the data is a Promise).
  • Not checking response.ok.
  • Using forEach with an async callback and expecting it to wait — it does not.

Practice

Build the small API explorer in the assignment. It exercises loading, error handling and rendering in one page.

Assignment

Assignment 08 — Async user explorer

Build a page that loads users from https://jsonplaceholder.typicode.com/users and lets the visitor filter them.

Requirements

  1. On page load, fetch the user list and render name, email and company for each user.
  2. While the request is running, show Loading….
  3. If the request fails, show an error message and a Try again button.
  4. A search box filters the already-loaded list by name or email (no new request).
  5. If the filter matches nothing, show No users found..
  6. A Reload button re-runs the fetch and replaces the list.

Constraints

  • Use async/await, not .then() chains.
  • Check response.ok before calling .json().
  • Render with createElement and textContent.
  • Clear the list with replaceChildren() before rendering new results.

Stretch

  • Debounce the filter by 200 ms.
  • Cancel the in-flight request with AbortController when Reload is clicked twice quickly.

Done when

  • A wrong URL (test it) shows the error state instead of a blank page.
  • Filtering never triggers a network request.

Learn this lesson with a mentor

Work through the Build Your Homepage series on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now