Fetching Data with fetch()
Making HTTP requests in the browser and handling the response with promises.
2 min read
fetch() is the browser's built-in way to make HTTP requests — loading data from an API, sending a form, or talking to any server over the network. It returns a promise, which is why it's the natural next step after covering promises and async/await.
A basic GET request
async function getUsers() {
const response = await fetch("https://api.example.com/users");
const users = await response.json();
return users;
}Two awaits, and it's easy to forget why: fetch() itself resolves as soon as the server sends back headers — before the full response body has necessarily arrived. response.json() is a second async step that reads and parses the body, and it returns its own promise.
fetch doesn't reject on HTTP errors
This is the single most common mistake with fetch: a 404 or 500 response does not cause the promise to reject. fetch only rejects on a genuine network failure (no connection, DNS failure, CORS block) — a "successful" request that came back with an error status still resolves normally.
async function getUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json();
}response.ok is true for any 2xx status and false otherwise — always check it (or response.status directly) before trusting the response body.
Sending data: POST requests
async function createUser(name) {
const response = await fetch("https://api.example.com/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
if (!response.ok) throw new Error("Failed to create user");
return response.json();
}The second argument is an options object: method sets the HTTP verb, headers describes the request (here, telling the server the body is JSON), and body carries the actual payload — which has to be a string, hence JSON.stringify().
Handling errors end to end
async function loadUsers() {
try {
const response = await fetch("https://api.example.com/users");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
console.error("Could not load users:", error);
return [];
}
}The catch block here handles both cases at once — a genuine network failure (which makes fetch itself reject) and a bad HTTP status (which the explicit throw converts into the same kind of catchable error) — so callers only need one error-handling path.
fetch is where promises, async/await, and error handling all come together in a single, extremely common real-world task. The next section covers organizing code across files with modules, and formalizing error handling with try/catch for cases beyond just network requests.