Fetch API

Course: JavaScript Intermediate

Lesson 14: Fetch API

Level

Intermediate

Estimated Reading Time

10–15 minutes

Goal

By the end of this lesson, the learner will be able to:

  • Use fetch to make HTTP requests and receive responses
  • Chain .then() and .catch() to handle success and error cases
  • Call response.json() to parse the response body and use the data

Frontend Development Context

The Fetch API is the browser's built-in tool for making HTTP requests. Almost every modern web app fetches data from an API — loading user profiles, product listings, weather data, or search results. Before fetch, developers used XMLHttpRequest, which was verbose and hard to read. fetch replaced it with a cleaner, Promise-based interface.

Understanding fetch with .then().catch() gives you the foundation for all network communication in JavaScript. Even if you later use libraries like Axios or frameworks that wrap fetch, the underlying behavior is the same. Mastering the raw API first means you can understand and debug network code at any level.

Explanation

fetch(url) sends an HTTP GET request to the specified URL and returns a Promise. Promises represent a value that will be available in the future — they are either fulfilled (resolved with a value) or rejected (with an error).

The first .then() receives the response object. This object contains metadata about the response (status code, headers) but not the body yet. To get the body as a parsed JavaScript object, you call response.json(), which also returns a Promise. You chain a second .then() to receive the parsed data.

An important detail: fetch only rejects its Promise for network failures (no connection, DNS errors). An HTTP error response like 404 Not Found or 500 Internal Server Error does NOT trigger .catch()fetch resolves the Promise normally and you must check response.ok or response.status manually to detect those errors.

.catch() at the end of the chain handles any rejected Promise in the chain — whether from a network failure or from manually throwing an error after checking response.ok.

.finally() runs after the chain completes regardless of success or failure. It is commonly used to hide a loading spinner.

Code Example

This example uses fetch with .then().catch() to load a user from a public API and render their name.

const userId = 1;
const url = `https://jsonplaceholder.typicode.com/users/${userId}`;

// Show loading state
document.querySelector("#status").textContent = "Loading...";

fetch(url)
  .then(function (response) {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.json(); // returns another Promise
  })
  .then(function (user) {
    // Data is available here
    document.querySelector("#status").textContent = "";
    document.querySelector("#username").textContent = user.name;
    document.querySelector("#email").textContent = user.email;
    console.log("Loaded user:", user.name);
  })
  .catch(function (error) {
    document.querySelector("#status").textContent = "Failed to load user.";
    console.error("Fetch error:", error.message);
  })
  .finally(function () {
    console.log("Fetch attempt complete");
  });

Code Explanation

fetch(url) starts the request and returns a Promise. The first .then() receives the response object. Before calling response.json(), we check response.ok — a boolean that is true for status codes 200–299. If the request returned a 404 or 500, response.ok is false and we manually throw an Error to skip to .catch().

return response.json() inside the first .then() returns a new Promise. Because we return it, the next .then() in the chain waits for it and receives the parsed object. This is the Promise chaining pattern — returning a Promise inside .then() passes control to the next .then().

The second .then(function(user) {...}) receives the parsed JSON as a plain JavaScript object. We update the DOM to display the user's name and email. This is where the data is finally ready to use.

.catch() handles any error thrown anywhere in the chain — both the manual throw new Error() from the status check and any genuine network failure. .finally() runs in all cases, making it the right place for cleanup like hiding a loading indicator.

Pattern Highlights

Positive Pattern: Always check response.ok in the first .then() and throw an error if it is false. Without this check, HTTP error responses appear as successes and your app silently fails.

⚠️ Neutral Note: response.json() must be awaited via a second .then(). The body of an HTTP response is a stream that has to be read asynchronously — you cannot access the data synchronously from the response object.

Negative Pattern: Do not nest .then() callbacks inside each other. Return the inner Promise and chain .then() flat instead. Nesting leads to the same indentation problems as callback hell.

Common Mistakes

Mistake 1: Forgetting that fetch does not reject on HTTP errors

Assuming a 404 response triggers .catch().

fetch("/api/missing-endpoint")
  .then(response => response.json()) // runs even for 404!
  .then(data => console.log(data))
  .catch(err => console.error(err)); // never runs for HTTP errors

Check response.ok in the first .then() and throw if the response is an error. fetch only rejects for network-level failures.

Mistake 2: Not returning response.json() inside .then()

Forgetting return causes the next .then() to receive undefined.

fetch(url)
  .then(response => {
    response.json(); // missing return!
  })
  .then(data => {
    console.log(data); // undefined
  });

response.json() returns a Promise. You must return it so the chain can pass the resolved value to the next .then().

Mistake 3: Using the parsed data outside the .then() chain

Trying to capture the fetched data in an outer variable by assignment inside .then().

let userData;
fetch(url).then(r => r.json()).then(data => { userData = data; });
console.log(userData); // undefined — fetch is asynchronous, this runs first

The console.log runs before the fetch completes. All code that depends on the fetched data must be placed inside the .then() callback or triggered from it.

Quick Recap

  • fetch(url) returns a Promise that resolves to a response object — not the data itself
  • Chain .then(response => response.json()) to parse the body, then a second .then(data => ...) to use the data
  • fetch does not reject on HTTP errors — check response.ok and throw manually if needed
  • .catch() handles all rejections in the chain; .finally() runs regardless of outcome

Ready to test your understanding? Take the quiz to reinforce what you learned.

© 2026 Ant Skillsv.26.07.07-23:01