The Fetch API
The fetch function is the modern, Promise-based way to make HTTP requests. It is available in browsers and in Node.js (v18+). It uses Promises, async/await, and error handling, so it pulls together everything we have covered in this chapter.
const response = await fetch("https://api.example.com/users/1");
const user = await response.json();
console.log(user.name); // "Alice"
Notice that fetch returns a Promise that resolves to a Response object. You then call a method on the response (like .json()) to read the body, and that returns another Promise. So one request takes two awaits. You will see this pattern in every fetch call.
Making GET Requests
A basic fetch call takes a URL and makes a GET request:
fetch("https://api.example.com/posts")
.then((response) => response.json())
.then((posts) => console.log(posts))
.catch((error) => console.error("Request failed:", error.message));
Or with async/await, which is how you will write most fetch code in practice:
async function loadPosts() {
const response = await fetch("https://api.example.com/posts");
const posts = await response.json();
console.log(posts);
}
The Response Object
The fetch Promise resolves to a Response object with several useful properties:
const response = await fetch("https://api.example.com/users/1");
console.log(response.status); // 200
console.log(response.statusText); // "OK"
console.log(response.ok); // true (status is 200-299)
console.log(response.headers.get("Content-Type")); // "application/json; charset=utf-8"
console.log(response.url); // "https://api.example.com/users/1"
The ok property is a boolean. It is true when status is in the 200–299 range. You will use it constantly for error checking.
Reading the Response Body
The body can only be read once, using one of these methods (each returns a Promise):
| Method | Returns | Use case |
|---|---|---|
.json() |
Parsed JavaScript object | JSON APIs |
.text() |
String | Plain text, HTML, XML |
.blob() |
Blob | Images, files |
.arrayBuffer() |
ArrayBuffer | Binary data |
.formData() |
FormData | Form submissions |
// Parse JSON
const data = await response.json();
// Read as plain text
const html = await response.text();
Fetch Does Not Reject on HTTP Errors
fetch only rejects on network failures (DNS errors, no internet, CORS issues). An HTTP error like 404 or 500 is still a successful network response, and the Promise fulfills with that error status.
// This does NOT throw or reject!
const response = await fetch("https://api.example.com/nonexistent");
console.log(response.status); // 404
console.log(response.ok); // false
You must check response.ok yourself:
async function fetchJSON(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
Sending Data with POST, PUT, and DELETE
To make requests other than GET, pass an options object as the second argument to fetch:
const response = await fetch("https://api.example.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "New Post",
content: "Hello, world!",
}),
});
The key options:
method: The HTTP method ("GET","POST","PUT","PATCH","DELETE")headers: An object (orHeadersinstance) with request headersbody: The request body — a string,FormData,Blob, etc. (not allowed for GET)
Updating a Resource with PUT
async function updatePost(id, data) {
const response = await fetch(`https://api.example.com/posts/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
const updated = await updatePost(1, { title: "Updated Title" });
Deleting a Resource
async function deletePost(id) {
const response = await fetch(`https://api.example.com/posts/${id}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
}
Keep in mind that DELETE responses often have no body, so do not call .json() unless you know the API returns one.
Setting Request Headers
Use the headers option to send metadata like authentication tokens or content types:
const response = await fetch("https://api.example.com/profile", {
headers: {
Authorization: "Bearer eyJhbGciOiJIUzI1NiIs...",
Accept: "application/json",
},
});
You can also use the Headers constructor, which provides methods like .append() and .has():
const headers = new Headers();
headers.append("Content-Type", "application/json");
headers.append("X-Custom-Header", "my-value");
const response = await fetch("https://api.example.com/data", { headers });
Canceling Requests with AbortController
Sometimes you need to cancel a request — a user navigates away, a timeout expires, or a new request supersedes the old one. The AbortController API handles this:
const controller = new AbortController();
fetch("https://api.example.com/large-data", {
signal: controller.signal,
})
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => {
if (error.name === "AbortError") {
console.log("Request was canceled");
} else {
console.error("Request failed:", error.message);
}
});
// Cancel the request
controller.abort();
The fetch Promise rejects with an AbortError.
Request Timeout with AbortSignal.timeout()
A common use case is canceling requests that take too long. AbortSignal.timeout() does that for you:
async function fetchWithTimeout(url, ms = 5000) {
const response = await fetch(url, {
signal: AbortSignal.timeout(ms),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
try {
const data = await fetchWithTimeout("https://api.example.com/slow", 3000);
} catch (error) {
if (error.name === "TimeoutError") {
console.error("Request timed out");
}
}
Putting It All Together
Here is an example that uses fetch with async/await, error handling, and parallel requests, all with the patterns from this chapter:
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
body: options.body ? JSON.stringify(options.body) : undefined,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get("Content-Type");
if (contentType?.includes("application/json")) {
return response.json();
}
return response.text();
}
get(endpoint) {
return this.request(endpoint);
}
post(endpoint, body) {
return this.request(endpoint, { method: "POST", body });
}
put(endpoint, body) {
return this.request(endpoint, { method: "PUT", body });
}
delete(endpoint) {
return this.request(endpoint, { method: "DELETE" });
}
}
// Usage with async/await and error handling
const api = new ApiClient("https://api.example.com");
try {
// Parallel requests with Promise.all
const [user, posts] = await Promise.all([
api.get("/users/1"),
api.get("/users/1/posts"),
]);
console.log(`${user.name} has ${posts.length} posts`);
// Create a new post
const newPost = await api.post("/posts", {
title: "Async JavaScript",
userId: user.id,
});
console.log("Created post:", newPost.id);
} catch (error) {
console.error("API error:", error.message);
}
This example uses Promises, Promise combinators, async/await, and error handling with fetch.