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
fetchto 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.okin 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 fetchdoes not reject on HTTP errors — checkresponse.okand throw manually if needed.catch()handles all rejections in the chain;.finally()runs regardless of outcome