The Fetch API
The Fetch API is the modern, promise-based way to make HTTP requests from JavaScript. It replaces the clunky XMLHttpRequest with a clean interface that fits naturally into async/await code. Available in every modern browser and in Node.js since v18, fetch is the default tool for talking to REST APIs, loading data, and submitting forms — so understanding how it handles responses and errors is essential.
Making a request
Calling fetch(url) kicks off a network request and immediately returns a promise. That promise resolves to a Response object as soon as the server sends back headers — it does not wait for the full body to download.
const promise = fetch("https://api.github.com/users/octocat");
console.log(promise); // Promise { <pending> }
The most common pattern is to await it inside an async function:
async function getUser() {
const response = await fetch("https://api.github.com/users/octocat");
console.log(response.status); // 200
console.log(response.ok); // true
}
getUser();
Output:
200
true
Reading the response
A Response doesn’t contain the body data directly — you read it through a method that returns another promise. Pick the method that matches the content type:
| Method | Returns | Use for |
|---|---|---|
.json() | parsed object/array | JSON APIs |
.text() | string | HTML, plain text, CSV |
.blob() | Blob | images, files, binary data |
.arrayBuffer() | ArrayBuffer | low-level binary |
.formData() | FormData | form submissions |
Because each of these returns a promise, you await them too:
async function getUser() {
const response = await fetch("https://api.github.com/users/octocat");
const data = await response.json();
console.log(`${data.name} has ${data.public_repos} public repos`);
}
getUser();
Output:
The Octocat has 8 public repos
A response body can only be read once. Calling
response.json()afterresponse.text()on the same response throws “body stream already read.” If you need the raw text and the parsed object, read the text once andJSON.parseit yourself.
Checking for errors
Here is the most important fetch gotcha: fetch only rejects on network failures — a dropped connection, DNS error, or CORS block. An HTTP error status like 404 or 500 still resolves the promise. You must inspect response.ok (true for status 200–299) yourself.
async function getUser(username) {
const response = await fetch(`https://api.github.com/users/${username}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status} - ${response.statusText}`);
}
return response.json();
}
try {
const user = await getUser("this-user-does-not-exist-xyz");
console.log(user.name);
} catch (err) {
console.error(err.message);
}
Output:
HTTP 404 - Not Found
The try/catch block here catches both network errors (from fetch rejecting) and HTTP errors (from the throw we added). Wrapping that logic in a reusable helper keeps every call site clean.
Sending data with POST
To do anything beyond a GET, pass a second argument: an options object. The key fields are method, headers, and body. Remember that body must be a string for JSON — call JSON.stringify on your object and set the matching Content-Type header.
async function createPost() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Learning fetch",
body: "It returns a promise!",
userId: 1,
}),
});
const created = await response.json();
console.log(`Created post #${created.id}`);
}
createPost();
Output:
Created post #101
Common options you can pass:
| Option | Description |
|---|---|
method | HTTP verb: GET (default), POST, PUT, PATCH, DELETE |
headers | request headers as an object or Headers instance |
body | payload string, FormData, Blob, or URLSearchParams |
signal | an AbortSignal to cancel the request |
credentials | omit, same-origin, or include for cookies |
mode | cors, no-cors, or same-origin |
Working with headers
You can read response headers through the response.headers object, which behaves like a Map:
const response = await fetch("https://api.github.com/users/octocat");
console.log(response.headers.get("content-type"));
Output:
application/json; charset=utf-8
A complete live example
This self-contained demo fetches a random activity suggestion from a public API, handles errors, and renders the result.
async function loadActivity() {
const output = document.getElementById("output");
try {
const res = await fetch("https://www.boredapi.com/api/activity");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
output.textContent = `Try this: ${data.activity}`;
} catch (err) {
output.textContent = `Request failed: ${err.message}`;
}
}
loadActivity();
Best practices
- Always check
response.ok(orresponse.status) before reading the body —fetchwill not throw on404or500. - Wrap requests in
try/catchso network failures and HTTP errors are handled in one place. - Read each response body only once; store the result if you need it twice.
- Set
Content-Type: application/jsonandJSON.stringifythe body when sending JSON. - Centralize fetch logic in a small wrapper (base URL, default headers, error handling) instead of repeating it.
- Use an
AbortControllerto cancel slow or stale requests and avoid memory leaks. - Prefer
async/awaitover chained.then()calls for readability.