Introduction to JavaScript · Lesson 4 of 5

Async/Await & Fetch API

Async JavaScript — Promises, Async/Await, and Fetch

JavaScript runs on a single thread. If you block it with a slow operation (file read, HTTP request, database query), the whole page freezes. Async code lets long operations run without blocking.


The Problem: Callbacks

The original async pattern. Still seen in older code and Node.js core APIs.

JavaScript
// Callback hell — hard to read, hard to error-handle
fetchUser(userId, (err, user) => {
  if (err) { handleError(err); return; }
  fetchOrders(user.id, (err, orders) => {
    if (err) { handleError(err); return; }
    fetchProducts(orders[0].productId, (err, product) => {
      if (err) { handleError(err); return; }
      renderUI(user, orders, product);
    });
  });
});

This "pyramid of doom" is why Promises were invented.


Promises

A Promise represents a value that will be available in the future (or an error).

JavaScript
// Creating a Promise
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = Math.random() > 0.5;
    if (success) resolve("Data loaded!");
    else reject(new Error("Something went wrong"));
  }, 1000);
});

// Consuming a Promise
promise
  .then(data => console.log(data))   // called on resolve
  .catch(err => console.error(err))  // called on reject
  .finally(() => console.log("Done")); // always called

Promise States

A Promise is always in one of three states:

  • Pending — initial state, waiting
  • Fulfilled — resolved successfully
  • Rejected — failed with an error

Once fulfilled or rejected, it's settled and never changes state.

Chaining Promises

JavaScript
fetchUser(1)
  .then(user => fetchOrders(user.id))    // return another Promise
  .then(orders => fetchProduct(orders[0].productId))
  .then(product => renderUI(product))
  .catch(err => handleError(err));       // catches any error in the chain

Promise.all, race, allSettled

JavaScript
// Promise.all — wait for ALL to resolve (fails fast if any reject)
const [user, products, settings] = await Promise.all([
  fetchUser(1),
  fetchProducts(),
  fetchSettings()
]);

// Promise.allSettled — wait for all, get results regardless of success/failure
const results = await Promise.allSettled([
  fetchUser(1),
  fetchMightFail(),
  fetchOther()
]);
results.forEach(result => {
  if (result.status === "fulfilled") console.log(result.value);
  if (result.status === "rejected") console.error(result.reason);
});

// Promise.race — first one wins (fulfilled or rejected)
const fastest = await Promise.race([
  fetch("https://api1.example.com/data"),
  fetch("https://api2.example.com/data")
]);

// Promise.any — first FULFILLED one wins (ignores rejections unless all fail)
const firstSuccess = await Promise.any([api1(), api2(), api3()]);

async/await

async/await is syntactic sugar over Promises. It makes async code look synchronous.

JavaScript
// async function always returns a Promise
async function fetchUserData(userId) {
  const user = await fetchUser(userId);       // wait for Promise to resolve
  const orders = await fetchOrders(user.id);  // sequential
  return { user, orders };
}

// Error handling with try/catch
async function loadDashboard(userId) {
  try {
    const user = await fetchUser(userId);
    const [orders, settings] = await Promise.all([
      fetchOrders(user.id),
      fetchSettings()
    ]);
    return { user, orders, settings };
  } catch (err) {
    console.error("Dashboard load failed:", err);
    throw err; // re-throw to let caller handle it
  } finally {
    setLoading(false);
  }
}

Parallel vs Sequential

JavaScript
// SEQUENTIAL — each awaits the previous (slow if independent)
const user = await fetchUser(1);
const posts = await fetchPosts(1);
// Total time = time(fetchUser) + time(fetchPosts)

// PARALLEL — start both at once (fast)
const [user, posts] = await Promise.all([fetchUser(1), fetchPosts(1)]);
// Total time = max(time(fetchUser), time(fetchPosts))

The Fetch API

fetch() makes HTTP requests. It returns a Promise.

JavaScript
// Basic GET
const response = await fetch("https://api.example.com/users");

// Always check if the request succeeded
if (!response.ok) {
  throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
}

const users = await response.json(); // parse JSON body

Full CRUD with Fetch

JavaScript
const BASE_URL = "https://api.example.com";

// GET — fetch a resource
async function getUser(id) {
  const res = await fetch(`${BASE_URL}/users/${id}`);
  if (!res.ok) throw new Error(`User ${id} not found`);
  return res.json();
}

// POST — create a resource
async function createUser(userData) {
  const res = await fetch(`${BASE_URL}/users`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(userData)
  });
  if (!res.ok) throw new Error("Failed to create user");
  return res.json();
}

// PUT — update a resource
async function updateUser(id, updates) {
  const res = await fetch(`${BASE_URL}/users/${id}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(updates)
  });
  if (!res.ok) throw new Error("Failed to update user");
  return res.json();
}

// DELETE
async function deleteUser(id) {
  const res = await fetch(`${BASE_URL}/users/${id}`, { method: "DELETE" });
  if (!res.ok) throw new Error("Failed to delete user");
}

// With auth token
async function fetchProtected(endpoint) {
  const token = localStorage.getItem("token");
  const res = await fetch(`${BASE_URL}${endpoint}`, {
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/json"
    }
  });
  if (res.status === 401) {
    // Token expired — redirect to login
    window.location.href = "/login";
    return;
  }
  if (!res.ok) throw new Error(`Request failed: ${res.status}`);
  return res.json();
}

Handling Different Response Types

JavaScript
const res = await fetch(url);

res.json()        // parse JSON
res.text()        // get as string
res.blob()        // get as Blob (for images, files)
res.formData()    // get as FormData
res.arrayBuffer() // get as binary data

// Reading response headers
res.headers.get("Content-Type");
res.headers.get("X-Request-Id");

AbortController — Cancellation

Use this to cancel in-flight requests (e.g., when user navigates away, or search debouncing).

JavaScript
// Cancel a request
const controller = new AbortController();

fetch("https://api.example.com/data", {
  signal: controller.signal
});

// Cancel it
controller.abort();

// Timeout pattern
async function fetchWithTimeout(url, timeoutMs = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const res = await fetch(url, { signal: controller.signal });
    clearTimeout(timeoutId);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  } catch (err) {
    if (err.name === "AbortError") throw new Error("Request timed out");
    throw err;
  }
}

Practical Pattern: Data Fetching Hook (UI)

JavaScript
// Generic data fetcher with loading/error states
async function fetchData(url, options = {}) {
  const { signal, onStart, onSuccess, onError } = options;

  onStart?.();
  try {
    const res = await fetch(url, { signal });
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
    const data = await res.json();
    onSuccess?.(data);
    return data;
  } catch (err) {
    if (err.name !== "AbortError") {
      onError?.(err);
    }
  }
}

// Usage on a button click
const controller = new AbortController();
searchBtn.addEventListener("click", async () => {
  const query = searchInput.value;
  await fetchData(`/api/search?q=${encodeURIComponent(query)}`, {
    signal: controller.signal,
    onStart: () => { spinner.style.display = "block"; },
    onSuccess: (data) => { renderResults(data); },
    onError: (err) => { showError(err.message); },
  });
  spinner.style.display = "none";
});

Key Takeaways

  1. Use async/await for readable async code — it's just Promises underneath
  2. Always check response.okfetch() only rejects on network errors, not HTTP errors
  3. Promise.all for parallel independent requests — much faster than sequential
  4. AbortController to cancel requests — prevents memory leaks and stale updates
  5. try/catch in async functions — unhandled Promise rejections will crash Node
  6. Never await inside a loop unless the operations must be sequential