Fetch API - Making HTTP Requests
Documentation for Fetch API - Making HTTP Requests.
Fetch API - Making HTTP Requests
What is the Fetch API?
Fetch API is a modern interface for making HTTP requests in JavaScript. It returns Promises and is cleaner than the older XMLHttpRequest.
Definition: The Fetch API provides a JavaScript interface for making HTTP requests and processing responses. It uses Promises, making it perfect for use with async/await syntax, and is the standard way to make network requests in modern web applications.
// Basic fetch
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error("Error:", error));
// With async/await (preferred)
async function getData() {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Error:", error);
}
}Why Use Fetch?
| Benefit | Description |
|---|---|
| Promise-based | Works seamlessly with async/await |
| Clean syntax | Much simpler than XMLHttpRequest |
| Built-in | No external libraries needed |
| Stream support | Can read response body as a stream |
| Modern features | AbortController, custom headers, CORS support |
Fetch vs XMLHttpRequest
| Feature | Fetch API | XMLHttpRequest |
|---|---|---|
| Syntax | Promise-based | Callback-based |
| Readability | Clean | Verbose |
| Browser support | Modern browsers | All browsers |
| CORS | Automatic | Manual setup |
| Streaming | Yes | Limited |
| Recommended | ✅ Yes | ❌ Legacy only |
Basic GET Request
// Simple GET
fetch("https://api.example.com/users")
.then((response) => response.json())
.then((users) => console.log(users));
// With async/await
async function getUsers() {
const response = await fetch("https://api.example.com/users");
const users = await response.json();
return users;
}
// With error handling
async function getUsers() {
try {
const response = await fetch("https://api.example.com/users");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const users = await response.json();
return users;
} catch (error) {
console.error("Fetch error:", error);
throw error;
}
}Response Object
The Response object contains information about the HTTP response.
async function checkResponse() {
const response = await fetch("https://api.example.com/data");
console.log(response.ok); // true if status 200-299
console.log(response.status); // 200, 404, 500, etc.
console.log(response.statusText); // 'OK', 'Not Found', etc.
console.log(response.headers); // Headers object
console.log(response.url); // Final URL (after redirects)
console.log(response.type); // 'basic', 'cors', etc.
console.log(response.redirected); // true if response is from redirect
}Response Methods
| Method | Description | Returns |
|---|---|---|
.json() | Parse as JSON | Promise<object> |
.text() | Get as text | Promise<string> |
.blob() | Get as Blob | Promise<Blob> |
.arrayBuffer() | Get as ArrayBuffer | Promise<ArrayBuffer> |
.formData() | Get as FormData | Promise<FormData> |
.clone() | Clone response | Response |
// JSON response
const data = await response.json();
// Text response (HTML, plain text)
const text = await response.text();
// Blob (for images, files)
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
// Clone response (body can only be read once!)
const clone = response.clone();
const data1 = await response.json();
const data2 = await clone.json(); // Read again from clonePOST Request
// POST with JSON
async function createUser(userData) {
const response = await fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const newUser = await response.json();
return newUser;
}
// Usage
const user = await createUser({
name: "John Doe",
email: "john@example.com",
});Request Methods
| Method | Purpose | Has Body |
|---|---|---|
| GET | Retrieve data | ❌ No |
| POST | Create data | ✅ Yes |
| PUT | Update (replace) | ✅ Yes |
| PATCH | Update (partial) | ✅ Yes |
| DELETE | Delete data | ❌ Usually no |
// GET - retrieve data
fetch("/api/users");
// POST - create new resource
fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "John", email: "john@example.com" }),
});
// PUT - replace entire resource
fetch("/api/users/1", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "John", email: "john@example.com", age: 30 }),
});
// PATCH - update specific fields
fetch("/api/users/1", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "New Name" }),
});
// DELETE - remove resource
fetch("/api/users/1", {
method: "DELETE",
});Request Headers
// Setting headers
async function fetchWithHeaders() {
const response = await fetch("https://api.example.com/data", {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer token123",
Accept: "application/json",
"X-Custom-Header": "custom-value",
},
});
return await response.json();
}
// Reading response headers
async function readHeaders() {
const response = await fetch("/api/data");
console.log(response.headers.get("Content-Type"));
console.log(response.headers.get("X-Custom-Response"));
// Iterate all headers
for (const [key, value] of response.headers) {
console.log(`${key}: ${value}`);
}
}Error Handling
Important: Fetch only rejects on network errors, NOT on HTTP errors (404, 500, etc.). You must check
response.okmanually.
// ❌ This won't catch 404 errors!
try {
const response = await fetch("/api/data");
const data = await response.json();
} catch (error) {
console.error(error); // Only catches network errors!
}
// ✅ Proper error handling
async function fetchData() {
try {
const response = await fetch("/api/data");
// Check if response is OK (status 200-299)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Fetch failed:", error);
throw error;
}
}
// Detailed error handling by status code
async function fetchWithDetailedErrors(url) {
try {
const response = await fetch(url);
if (!response.ok) {
switch (response.status) {
case 400:
throw new Error("Bad request - check your data");
case 401:
throw new Error("Unauthorized - please login");
case 403:
throw new Error("Forbidden - you don't have permission");
case 404:
throw new Error("Resource not found");
case 500:
throw new Error("Server error - try again later");
default:
throw new Error(`HTTP error! status: ${response.status}`);
}
}
return await response.json();
} catch (error) {
console.error("Error:", error.message);
throw error;
}
}Request Options
const options = {
method: "POST", // GET, POST, PUT, PATCH, DELETE
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data), // Request body
mode: "cors", // cors, no-cors, same-origin
credentials: "include", // include, same-origin, omit
cache: "no-cache", // default, no-cache, reload, force-cache
redirect: "follow", // follow, manual, error
referrerPolicy: "no-referrer",
signal: controller.signal, // For aborting requests
};
fetch(url, options);Timeout Implementation
Fetch doesn't have built-in timeout, but you can implement it using AbortController.
// Timeout with AbortController
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal,
});
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
throw new Error("Request timeout");
}
throw error;
}
}
// Usage
try {
const data = await fetchWithTimeout("/api/data", 3000);
} catch (error) {
console.error(error.message); // 'Request timeout' if > 3s
}Canceling Requests
// Using AbortController
const controller = new AbortController();
fetch("/api/data", {
signal: controller.signal,
})
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => {
if (error.name === "AbortError") {
console.log("Request canceled");
}
});
// Cancel the request
controller.abort();
// Practical: Cancel previous request on new search
let controller = null;
async function search(query) {
// Cancel previous request
if (controller) {
controller.abort();
}
// Create new controller for this request
controller = new AbortController();
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: controller.signal,
});
return await response.json();
} catch (error) {
if (error.name !== "AbortError") {
throw error;
}
return null; // Request was canceled
}
}Uploading Files
// Upload single file
async function uploadFile(file) {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/upload", {
method: "POST",
body: formData, // Don't set Content-Type header! Browser sets it automatically
});
return await response.json();
}
// Upload multiple files
async function uploadMultiple(files) {
const formData = new FormData();
for (const file of files) {
formData.append("files", file);
}
const response = await fetch("/api/upload-multiple", {
method: "POST",
body: formData,
});
return await response.json();
}
// Upload with additional data
async function uploadWithData(file, metadata) {
const formData = new FormData();
formData.append("file", file);
formData.append("title", metadata.title);
formData.append("description", metadata.description);
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});
return await response.json();
}
// File input usage
const fileInput = document.getElementById("fileInput");
fileInput.addEventListener("change", async (e) => {
const file = e.target.files[0];
const result = await uploadFile(file);
console.log("Upload result:", result);
});Interview Questions & Answers
Q1: What is the Fetch API and how does it differ from XMLHttpRequest?
The Fetch API is a modern JavaScript interface for making HTTP requests that was introduced to replace the older XMLHttpRequest. The key difference is that Fetch uses Promises, which makes it work seamlessly with async/await syntax, while XMLHttpRequest uses callbacks. This makes Fetch code much cleaner and easier to read. Fetch also has a simpler API with sensible defaults, automatic CORS handling, and built-in support for streaming responses. However, XMLHttpRequest still works in all browsers including very old ones, while Fetch requires a polyfill for older browsers. For any new project, Fetch is the recommended approach.
Q2: Why doesn't fetch() reject on HTTP errors like 404 or 500?
Fetch only rejects when there's a network failure, like when the server is unreachable or there's no internet connection. HTTP error responses like 404 Not Found or 500 Server Error are still valid HTTP responses - the request succeeded in reaching the server and getting a response back. This design gives you more control over how to handle different status codes. To check for HTTP errors, you need to examine the response.ok property, which is true for status codes 200-299 and false otherwise. If you need error behavior, you must manually check this and throw an error yourself.
Q3: How do you implement timeout with fetch()?
Fetch doesn't have a built-in timeout option, so you need to implement it using AbortController. First, create an AbortController instance and pass its signal property to the fetch options. Then use setTimeout to call controller.abort() after your desired timeout period. When the request is aborted, fetch will reject with an AbortError that you can catch and handle as a timeout. This same pattern also allows you to cancel requests when users navigate away or start a new search. The AbortController is a general-purpose way to cancel any operation that accepts an AbortSignal.
Q4: What's the difference between PUT and PATCH methods?
| Feature | PUT | PATCH |
|---|---|---|
| Purpose | Full replacement | Partial update |
| Sends | Complete resource | Only changed fields |
| Idempotent | Yes | Yes |
| Example | Replace entire user object | Update only user's email |
PUT replaces the entire resource with the new data you send, so you must include all fields. PATCH updates only the specific fields you send, leaving other fields unchanged. For example, to update a user's email, PUT would require sending the entire user object with all fields, while PATCH would only require sending the email field. Both are idempotent, meaning multiple identical requests have the same effect as a single request.
Practical Examples
// Example 1: Complete CRUD operations
const API_URL = "https://api.example.com";
async function getUsers() {
const response = await fetch(`${API_URL}/users`);
if (!response.ok) throw new Error("Failed to fetch users");
return await response.json();
}
async function createUser(user) {
const response = await fetch(`${API_URL}/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(user),
});
if (!response.ok) throw new Error("Failed to create user");
return await response.json();
}
async function updateUser(id, updates) {
const response = await fetch(`${API_URL}/users/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updates),
});
if (!response.ok) throw new Error("Failed to update user");
return await response.json();
}
async function deleteUser(id) {
const response = await fetch(`${API_URL}/users/${id}`, {
method: "DELETE",
});
if (!response.ok) throw new Error("Failed to delete user");
}
// Example 2: Retry logic
async function fetchWithRetry(url, options = {}, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
// Example 3: Parallel requests
async function getDashboard() {
const [users, posts, comments] = await Promise.all([
fetch("/api/users").then((r) => r.json()),
fetch("/api/posts").then((r) => r.json()),
fetch("/api/comments").then((r) => r.json()),
]);
return { users, posts, comments };
}
// Example 4: Search with cancel previous
let searchController;
async function search(query) {
// Cancel previous search
if (searchController) {
searchController.abort();
}
searchController = new AbortController();
try {
const response = await fetch(`/api/search?q=${query}`, {
signal: searchController.signal,
});
if (!response.ok) throw new Error("Search failed");
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
return null; // Search was canceled
}
throw error;
}
}
// Example 5: Download with progress
async function downloadWithProgress(url, onProgress) {
const response = await fetch(url);
const contentLength = response.headers.get("Content-Length");
const total = parseInt(contentLength, 10);
let loaded = 0;
const reader = response.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.length;
onProgress(Math.round((loaded / total) * 100));
}
return new Blob(chunks);
}