What is the Fetch API?
In JavaScript, Fetch API is a modern way to make network requests, like getting data from a server.
Before Fetch, developers used XMLHttpRequest (XHR)
which was more complex and harder to use.
Fetch is a more powerful and flexible replacement for the older XMLHttpRequest
.
Fetch is a browser API for making HTTP requests and returns a Promise that resolves to a Response
object.
You can think of it as a way for your web page to talk to servers and get data without refreshing the entire page.
It’s built into all modern browsers and is used for easily requesting resources such as JSON, text, images, or any other data from a URL.
Basic Syntax
fetch(url, options)
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error("Error:", error));
Here,
- url (string): The URL to request.
- options (object): Optional settings like method, headers, body, etc.
How Fetch Works
- Call
fetch()
with a URL (and optional options). - It returns a Promise that resolves to a
Response
object once the request completes. - Check if the response is successful (
response.ok
). - Extract the response data using methods like
.json()
,.text()
, or.blob()
. - Handle errors if the network fails or the server returns an error status.
Making Your First Request
Simple GET Request
A GET request simply asks the server for some data.
// Fetch data from a public API
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then((response) => response.json()) // Convert response to JSON
.then((data) => {
console.log(data); // Work with the actual data
})
.catch((error) => {
console.error("Something went wrong:", error);
});
Here,
fetch(...)
sends a request to the server..then(response => response.json())
converts the raw response into a usable JavaScript object.- The second
.then(...)
lets you use the actual data. .catch(...)
handles any errors, like network issues.
Using Async/Await (Recommended)
The same thing, but with async/await
, as it is much cleaner and easier to read.
async function fetchPost() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts/1"
);
const data = await response.json(); // Parse JSON
console.log(data); // Log the result
} catch (error) {
console.error("Error fetching data:", error);
}
}
fetchPost();
It looks like normal code and easier to debug and handle errors with try/catch
.
Understanding the Response Object
When you make a fetch request, it doesn't directly give you the data, it first gives you a Response
object.
This object holds important details about the response from the server, like status codes, headers, and the final URL.
async function examineResponse() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
console.log(response.status); // 200
console.log(response.statusText); // "OK"
console.log(response.ok); // true (status 200-299)
console.log(response.headers); // all response headers
console.log(response.url); // the final URL of the request
const data = await response.json(); // Convert to JSON and finally, get the real data
}
Here,
response.status
: The HTTP status code (e.g., 200, 404, 500).response.statusText
: A short description (e.g., "OK", "Not Found").response.ok
: true if the status code is between 200 and 299, success!response.headers
: Metadata sent by the server (like content type, cache info).response.url
: The actual URL used for the request (could include redirects).
Once you check the response, you usually parse the body using .json()
, .text()
, or .blob()
depending on the type of data.
Note: Always check response.ok
before using the data, just in case the request failed silently.
Different Ways to Parse Response Data
When you use fetch, the response body isn’t immediately usable. You need to parse it based on what type of data you're expecting.
Here's how to do that for common formats:
JSON Data
Use .json()
when you're expecting a JSON response (like most APIs return).
const response = await fetch("/api/users");
const jsonData = await response.json(); // Converts the body into a JavaScript object
Use this when the server responds with application/json
.
Text Data
Use .text()
when the response is plain text (like a message or raw HTML).
const response = await fetch("/api/message");
const textData = await response.text(); // Gets raw string data
Use this when the server sends simple text, not structured JSON.
Blob Data (for files)
Use .blob()
when you're downloading things like images, PDFs, or videos.
const response = await fetch("/api/image");
const blobData = await response.blob(); // Creates a Blob object for file-like data
Use this when the response contains binary file data.
Note: You can only use one parser method per response, once you call .json()
, .text()
, or .blob()
, you can’t read it again.
Request Options in Fetch
The fetch()
function can take two arguments:
- The URL you want to request
- An options object that lets you customize the request (method, headers, body, etc.)
Basic structure:
const options = {
method: "POST", // Type of request: GET, POST, PUT, DELETE, etc.
headers: {
"Content-Type": "application/json", // Tells the server you're sending JSON
Authorization: "Bearer token123", // Auth token (if required)
},
body: JSON.stringify(data), // Convert JS object to JSON string
credentials: "include", // Sends cookies along with the request
cache: "no-cache", // Forces fetch to get fresh data, ignoring cached versions
redirect: "follow", // Auto-follow redirects (default)
};
fetch("https://api.example.com/data", options);
Here,
method
: HTTP method (GET
,POST
,PUT
,DELETE
, etc.)headers
: Key-value pairs to describe the request (likeContent-Type
)body
: Data you want to send (use withPOST
orPUT
) |credentials
: Include cookies or auth info (include
,same-origin
, oromit
) |cache
: Controls caching (default
,no-cache
,reload
, etc.) |redirect
: Follow redirects automatically (follow
), or block them (error
) |
Example:
async function submitData(data) {
const response = await fetch("https://api.example.com/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer your_token_here",
},
body: JSON.stringify(data),
credentials: "include",
cache: "no-cache",
redirect: "follow",
});
const result = await response.json();
console.log(result);
}
HTTP Methods with Fetch
The fetch()
API supports all HTTP methods: GET
, POST
, PUT
, DELETE
, etc. You just need to set the method
in the options
object.
GET Request (Read Data)
The default method for fetch()
is GET
. You don’t have to specify it unless you want to be explicit.
This is used when you want to fetch (read) data from the server.
// These are equivalent
const response1 = await fetch("/api/users");
const response2 = await fetch("/api/users", { method: "GET" });
Here,
fetch("/api/users")
sends a GET request by default.{ method: "GET" }
is optional but can be added for clarity.- This type of request retrieves data from the server.
- You usually follow it with
await response.json()
to access the actual data.
POST Request (Create Data)
The POST
method is used when you want to send new data to the server, like adding a new item or record.
async function createUser(userData) {
try {
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json", // Tell the server you're sending JSON
},
body: JSON.stringify(userData), // Convert JS object to JSON string
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error("Error creating user:", error);
}
}
// Usage
const newUser = { name: "John", email: "john@example.com" };
createUser(newUser);
Here,
- POST is used to create a new user on the server.
Content-Type: application/json
tells the server you're sending JSON data.userData
is converted to JSON usingJSON.stringify
.response.ok
checks if the response is okay.- Returns the result as JSON if successful.
- Errors are caught and logged in
catch
.
PUT Request (Update Data)
The PUT
method is used when you want to update an existing item.
async function updateUser(userId, userData) {
const response = await fetch(`/api/users/${userId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(userData),
});
return response.json();
}
Here,
- PUT is used to update an existing user.
- It targets a specific user using their ID in the URL.
- Sends updated data in JSON format in the request body.
- Returns the updated user object as a JSON response.
DELETE Request (Remove Data)
The DELETE
method is used when you want to remove data from the server.
async function deleteUser(userId) {
const response = await fetch(`/api/users/${userId}`, {
method: "DELETE",
});
if (response.ok) {
console.log("User deleted successfully");
}
}
Here,
- DELETE is used to remove a user based on their ID.
- Sends a request to the server using method: "DELETE".
- Checks if the response was successful (
response.ok
). - Logs a success message if the deletion worked.
Working with Headers in Fetch API
Headers are used to send metadata, like content type, authorization tokens, or custom data.
Reading Response Headers
When you make a fetch()
request, the server might send back headers along with the response. You can inspect them using the response.headers
object.
async function checkHeaders() {
const response = await fetch("/api/data");
// Get a specific header
// get() reads the value of a header
const contentType = response.headers.get("Content-Type");
console.log("Content-Type:", contentType);
// Check if a custom header is present
// has() checks if a header exists
if (response.headers.has("X-Custom-Header")) {
console.log("Custom header found");
}
// Loop through all headers with for...of
for (const [key, value] of response.headers) {
console.log(`${key}: ${value}`);
}
}
Setting Request Headers
You often need to send headers when making a request, like when you're sending JSON or adding auth tokens.
const response = await fetch("/api/protected", {
headers: {
Authorization: "Bearer " + token, // Auth token
"X-API-Key": "your-api-key", // Custom API key
"Content-Type": "application/json", // Tells server the body is JSON
},
});
Why this matters:
Authorization
is often needed to access protected routes.Content-Type: application/json
tells the server you’re sending JSON.- Custom headers (like
"X-API-Key"
) can be used to identify apps or control access.
Error Handling with Fetch
Unlike some older APIs, fetch()
doesn’t automatically throw an error for HTTP failures like 404 or 500. It only throws on network failures (like no internet).
Using fetch()
without proper error handling can lead to silent failures and hard-to-debug issues. So you need to manually check the response and handle errors properly using try/catch
.
Basic Error Handling
async function fetchWithErrorHandling() {
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
if (error.name === "TypeError") {
// Likely a network issue
console.error("Network error:", error.message);
} else {
// Any other error
console.error("Error:", error.message);
}
}
}
Here,
fetch("/api/data")
sends the request.if (!response.ok)
checks if the status is not in the 200–299 range.throw new Error(...)
lets you handle bad HTTP responses.- The
catch
block catches:TypeError
: likely a network failure or blocked request.- Other errors: like a manual
throw
.
Comprehensive Error Handling
Sometimes, APIs send back error messages in JSON format (like { "message": "User not found" }) when something goes wrong.
To handle these kinds of errors properly, you can write a more powerful error-handling function like this:
async function robustFetch(url, options = {}) {
try {
const response = await fetch(url, options);
// Handle HTTP errors
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP ${response.status}: ${response.statusText}`
);
}
return await response.json();
} catch (error) {
if (error.name === "TypeError") {
// Handle network issues
throw new Error("Network connection failed");
}
throw error; // Re-throw for caller to handle
}
}
Here,
async function robustFetch(url, options = {}) {
:- Defines an
async
function namedrobustFetch
. - Takes a
url
and an optionaloptions
object (like method, headers, body). - It's a wrapper around the regular
fetch()
function, but smarter.
- Defines an
const response = await fetch(url, options);
await fetch(...)
sends the request and waits for the response.- The result is stored in
response
.
if (!response.ok) {...}
response.ok
istrue
for 2xx status codes (like 200, 201).- If the status is not ok (like 404, 500):
- Try to parse the error response as JSON.
- If the API sends
{ "message": "Something went wrong" }
, you use that. - If parsing fails, fallback to a generic message like
"HTTP 404: Not Found"
. - Then you throw a custom
Error
.
return await response.json();
If the response was successful, parse and return the JSON data.} catch (error) {...}
- If there’s a network problem (like no internet or DNS issue), fetch throws a
TypeError
. - You catch that and throw a clearer message:
"Network connection failed"
. - For any other kind of error, you re-throw it so the caller can handle it.
- If there’s a network problem (like no internet or DNS issue), fetch throws a
Canceling a Fetch Request
Sometimes you may want to cancel a fetch request if it takes too long or the user navigates away.
You can do this with AbortController
.
const controller = new AbortController();
const signal = controller.signal;
fetch("https://jsonplaceholder.typicode.com/posts", { signal })
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => {
if (error.name === "AbortError") {
console.log("Fetch aborted");
} else {
console.error("Fetch error:", error);
}
});
// Abort the request after 2 seconds
setTimeout(() => controller.abort(), 2000);
Here,
AbortController
is a built-in browser API.- It lets you cancel DOM requests like fetch.
signal
is used to link the controller to the request.- You make a
fetch()
call and pass{ signal }
to associate it with the controller. - If the request is aborted,
.catch()
will run. - You check if
error.name === "AbortError"
to handle it differently from other fetch errors. setTimeout(() => controller.abort(), 2000);
cancels the fetch request after 2 seconds.- The
.catch()
block will run and log "Fetch aborted".
Common Mistakes to Avoid
- Not checking response.ok: Fetch doesn't throw errors for HTTP error status codes.
- Forgetting to parse the response: Always call
.json()
,.text()
, etc. - Not handling network errors: Always use
try/catch
or.catch()
. - Setting wrong Content-Type: Don't set Content-Type for FormData.
- Not handling loading states: Show loading indicators in your UI.
Best Practices for Using Fetch API
1. Always handle errors
When you make a request, things can go wrong, network issues, server errors, or invalid responses. Without error handling, your app can crash silently or behave unpredictably.
// Good: Use try/catch and check response status
try {
const response = await fetch("/api/data");
if (!response.ok) throw new Error("Request failed");
const data = await response.json();
} catch (error) {
console.error("Error:", error);
}
// Bad: No error handling, your app might crash or misbehave
const response = await fetch("/api/data");
const data = await response.json(); // If this fails, there is no catch
2. Check response status
fetch
does not throw errors for HTTP status like 404 or 500. You need to check response.ok
manually.
// Good: Throw an error if status is not OK (200-299)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Bad: Ignoring status might lead to wrong data or silent failures
const response = await fetch("/api/data");
3. Set appropriate headers
Headers tell the server what kind of data you are sending.
// For sending JSON data
fetch("/api/data", {
method: "POST",
headers: {
"Content-Type": "application/json", // Important for JSON payloads
},
body: JSON.stringify(data),
});
// For sending form data (files, etc.) with FormData
const formData = new FormData();
fetch("/api/upload", {
method: "POST",
body: formData, // DO NOT set Content-Type here, the browser sets it automatically
});
4. Use async/await for readability
async/await
makes asynchronous code easier to read and maintain than chaining .then()
.
// Good: Clear and easy to understand
async function fetchData() {
try {
const response = await fetch("/api/data");
const data = await response.json();
return data;
} catch (error) {
console.error("Error:", error);
}
}
// Less readable: Promises chaining with .then()
function fetchData() {
return fetch("/api/data")
.then((response) => response.json())
.then((data) => data)
.catch((error) => console.error("Error:", error));
}