π Fetch API Fundamentals
Almost every app you build will need to talk to a server β to load a list of products, save a form, or check who's logged in. The fetch() function is the browser's built-in way to do exactly that. This lesson takes you from your very first request to a solid, error-aware pattern you'll reuse everywhere.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain what the Fetch API is and how it improved on
XMLHttpRequest - Make a basic GET request and understand fetch's two-stage promise flow
- Check
response.okand parse a body withresponse.json() - Rewrite fetch code cleanly using async/await with
try/catch - Recognize fetch's most common beginner gotcha: HTTP errors do not reject the promise
Estimated Time: 30β40 minutes β’ Difficulty: Intermediate
Hands-on: Build a reusable getJSON() helper against a live public API.
In This Lesson
What Is the Fetch API?
The Fetch API is the modern, built-in browser interface for making network requests. Its centerpiece is a single global function β fetch() β that sends an HTTP request and returns a Promise. Because it is promise-based, it plugs directly into the async/await syntax you met earlier in this module, giving you clean, readable code without a tangle of callbacks.
π‘ An analogy: Think of fetch() as ordering takeout. You place the order (send the request) and get a receipt immediately (the promise). The receipt is not your food β it's a promise that food is coming. Later the delivery arrives (the response), and only then do you unwrap the bag to see what's actually inside (the body). Fetch works in exactly these two steps.
Fetch replaced an older, clunkier tool called XMLHttpRequest (XHR). Here is the short version of how browser networking evolved:
- Full page reloads β every interaction meant reloading the whole page.
- XMLHttpRequest (2005) β enabled AJAX (updating part of a page without a reload) but used a verbose, callback-heavy API.
- jQuery
$.ajax(2006) β smoothed over XHR's rough edges, but required an external library. - Fetch API (2015) β native, promise-based, and now supported in every modern browser.
π Key Terms
Promise: an object representing a value that isn't ready yet β it will eventually resolve with a result or reject with an error.
Response: the object fetch gives you once the server replies; it holds the status, headers, and a stream for the body.
Endpoint: a specific URL on a server that responds to requests, e.g. https://api.example.com/users.
Your First Request
The simplest possible use of fetch is a GET request to retrieve some data. Here it is with the classic .then() chain, so you can see every step explicitly:
// A basic GET request
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => {
// Stage 1: the server has replied with headers + status.
// fetch does NOT throw on 404/500, so we check ourselves:
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // start reading & parsing the body
})
.then((data) => {
// Stage 2: the body has finished downloading and parsing.
console.log('Data received:', data);
})
.catch((error) => {
console.error('Fetch failed:', error);
});
Notice there are two .then() blocks. That is not an accident β it reflects how fetch actually works, which is the single most important idea in this lesson.
The Two-Stage Promise Flow
A fetch call resolves in two separate stages. The first promise resolves as soon as the response headers arrive β before the body has finished downloading. Reading the body (with response.json(), response.text(), and friends) returns a second promise, because the body may still be streaming in.
Response when headers arrive, then to the parsed data when the body finishes. That is why you see two awaits (or two .then()s) in typical fetch code.Once you internalize this, the shape of every fetch you write makes sense: await the response, check it, then await the body.
Fetch with Async/Await
The .then() chain works, but async/await reads like ordinary step-by-step code and makes error handling with try/catch natural. This is the style you should reach for by default:
async function fetchTodo(id) {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log('Data received:', data);
return data;
} catch (error) {
console.error('Fetch failed:', error);
throw error; // re-throw so the caller can react too
}
}
fetchTodo(1);
Read it top to bottom: send the request, wait for the response, verify it succeeded, wait for the body, return the data. The try/catch catches both network failures and the error we throw ourselves on a bad status.
π‘ Why re-throw?
Logging an error and swallowing it hides problems from whatever called your function. Re-throwing (or returning a clear failure value) lets the caller decide how to respond β show a toast, retry, or fall back to cached data.
The Big Gotcha: HTTP Errors Don't Reject
This trips up nearly every newcomer, so it deserves its own section. A fetch promise only rejects on a network-level failure β no connection, DNS failure, CORS block, or a request you aborted. An HTTP error status such as 404 Not Found or 500 Internal Server Error is considered a successful round trip: the server was reached and it answered. The promise happily resolves.
β οΈ This looks correct but is broken
// β BUG: a 404 or 500 slips through as if it succeeded
async function brokenGet(url) {
const response = await fetch(url);
const data = await response.json(); // may throw on an error page's HTML!
return data;
}
If the server returns a 404 with an HTML error page, response.json() throws a confusing parse error β or worse, an error body parses fine and your app treats failure as data.
β
The fix: always check response.ok
async function safeGet(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
response.ok is true only for statuses in the 200β299 range. Make this check a reflex in every request you write.
Reading the Response Body
The Response object can hand you the body in several formats. You pick the method that matches what the server sent. Each returns a promise and β importantly β can only be called once, because the body is a stream that is consumed as you read it.
| Method | Resolves to | Use when the response is⦠|
|---|---|---|
response.json() | JavaScript object/array | JSON β by far the most common for APIs |
response.text() | String | Plain text, HTML, CSV, or XML |
response.blob() | Blob | Binary files like images or PDFs |
response.formData() | FormData | A multipart/form-data reply |
response.arrayBuffer() | ArrayBuffer | Raw bytes you'll process yourself |
For example, fetching an image as a Blob and showing it on the page:
async function showImage(url, imgElement) {
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed: ${response.status}`);
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
imgElement.src = objectUrl;
// Free the memory once the browser has loaded it
imgElement.onload = () => URL.revokeObjectURL(objectUrl);
}
β οΈ Bodies are single-use
Calling both response.json() and response.text() on the same response throws "body stream already read". If you genuinely need the body twice, call response.clone() first and read each copy once.
Hands-on Exercise
ποΈ Build a reusable getJSON() helper
Objective: Write one small function you can drop into any project to fetch JSON safely, then use it against a real public API that needs no key.
Instructions:
- Write an
asyncfunctiongetJSON(url)that fetches the URL, throws a clear error if!response.ok, and otherwise returns the parsed JSON. - Call it with
https://jsonplaceholder.typicode.com/usersand log how many users came back. - Call it again with a deliberately wrong path like
.../userzzzand confirm your error branch runs (not a silent success). - Wrap the calls in
try/catchand print a friendly message on failure.
π‘ Hint
Remember the reflex from this lesson: a 404 still resolves. Your if (!response.ok) check is what turns that 404 into an actual thrown error your catch can see.
β Sample solution
async function getJSON(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request to ${url} failed: ${response.status}`);
}
return response.json();
}
async function main() {
try {
const users = await getJSON('https://jsonplaceholder.typicode.com/users');
console.log(`Loaded ${users.length} users`);
// This path 404s -> our check throws -> catch runs
await getJSON('https://jsonplaceholder.typicode.com/userzzz');
} catch (error) {
console.error('Something went wrong:', error.message);
}
}
main();
π― Quick Quiz
Question 1: A server responds with status 404. What does the fetch() promise do?
Question 2: Why does typical fetch code contain two await statements?
Question 3: You call response.json() and then response.text() on the same response. What happens?
Best Practices
β Do
- Check
response.ok(or the status) on every request before reading the body. - Prefer
async/awaitwithtry/catchfor readability. - Centralize fetching in a small helper so error handling lives in one place.
- Match the body method to the content:
json()for JSON,blob()for binary.
β οΈ Don't
- Don't assume a resolved promise means success β a 500 resolves too.
- Don't read the same body twice without
clone(). - Don't reach for a library like axios before you understand fetch; the fundamentals here transfer directly.
Summary & Quiz
π Key Takeaways
- Fetch is promise-based and native to every modern browser β no library needed.
- It resolves in two stages: first a
Response, then the parsed body. - HTTP errors do not reject β always check
response.ok. - Pick the right body reader (
json,text,blobβ¦); each is single-use. async/await+try/catchgives you the cleanest, most robust code.
π Further Reading
π What's Next?
You can now make and verify a request. Next we dig into the second argument to fetch β the options object β where you set the HTTP method, headers, body, credentials, and more to control exactly how a request is sent.
π Nice work!
The two-stage flow and the response.ok reflex are the whole foundation. Everything else in this module builds on them.