Fetch API
The Fetch API provides a modern, promise-based way to make HTTP requests in JavaScript. It's more powerful and flexible than the older XMLHttpRequest, and works perfectly with async/await syntax.
Introduction to Fetch API
The fetch() function returns a Promise that resolves to a Response object. The basic syntax is simple:
// Basic GET request
fetch("https://api.example.com/users")
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Error:", error));
// With async/await (preferred)
async function getUsers() {
try {
const response = await fetch("https://api.example.com/users");
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Error:", error);
}
}
Important: The Fetch API only rejects a promise when a network error occurs. HTTP errors (like 404 or 500) do NOT cause rejection - you must check the response.ok property or response.status manually.
Making GET Requests
GET requests are used to retrieve data from a server:
// Simple GET request
async function fetchUser(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
// Check if request was successful
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error("Failed to fetch user:", error);
throw error;
}
}
// GET request with query parameters
async function searchUsers(query, page = 1) {
const params = new URLSearchParams({
q: query,
page: page,
limit: 10
});
const response = await fetch(`https://api.example.com/users?${params}`);
if (!response.ok) {
throw new Error("Search failed");
}
return await response.json();
}
// Usage
const results = await searchUsers("john", 1);
console.log(results);
POST Requests - Creating Data
POST requests send data to create new resources on the server:
async function createUser(userData) {
try {
const response = await fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer your-token-here"
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const newUser = await response.json();
return newUser;
} catch (error) {
console.error("Failed to create user:", error);
throw error;
}
}
// Usage
const userData = {
name: "John Doe",
email: "john@example.com",
age: 30
};
const newUser = await createUser(userData);
console.log("Created user:", newUser);
Tip: Always set the appropriate Content-Type header when sending data. For JSON data, use "application/json". For form data, use "application/x-www-form-urlencoded" or "multipart/form-data".
PUT and PATCH Requests - Updating Data
PUT replaces an entire resource, while PATCH updates specific fields:
// PUT - Replace entire resource
async function updateUser(userId, userData) {
const response = await fetch(`https://api.example.com/users/${userId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(userData)
});
if (!response.ok) {
throw new Error("Update failed");
}
return await response.json();
}
// PATCH - Update specific fields
async function updateUserEmail(userId, newEmail) {
const response = await fetch(`https://api.example.com/users/${userId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ email: newEmail })
});
if (!response.ok) {
throw new Error("Email update failed");
}
return await response.json();
}
// Usage
await updateUserEmail(123, "newemail@example.com");
DELETE Requests - Removing Data
DELETE requests remove resources from the server:
async function deleteUser(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`, {
method: "DELETE",
headers: {
"Authorization": "Bearer your-token-here"
}
});
if (!response.ok) {
throw new Error(`Failed to delete user: ${response.status}`);
}
// Some APIs return no content (204 status)
if (response.status === 204) {
return { success: true };
}
return await response.json();
} catch (error) {
console.error("Delete failed:", error);
throw error;
}
}
// Usage with confirmation
async function deleteUserWithConfirmation(userId) {
if (confirm("Are you sure you want to delete this user?")) {
await deleteUser(userId);
console.log("User deleted successfully");
}
}
Request Headers and Options
Fetch accepts an options object with many configuration possibilities:
async function advancedFetch() {
const response = await fetch("https://api.example.com/data", {
method: "POST",
// Headers
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer token123",
"X-Custom-Header": "custom-value"
},
// Body data
body: JSON.stringify({ key: "value" }),
// Credentials (cookies)
credentials: "include", // "omit", "same-origin", or "include"
// Cache control
cache: "no-cache", // "default", "reload", "no-cache", "force-cache"
// Redirect handling
redirect: "follow", // "follow", "error", or "manual"
// Referrer
referrer: "client",
// Integrity check
integrity: "sha256-...",
// Request mode
mode: "cors" // "cors", "no-cors", "same-origin"
});
return await response.json();
}
Response Handling
The Response object provides multiple ways to read the response body:
async function handleDifferentResponses() {
// JSON response
const jsonResponse = await fetch("/api/users");
const jsonData = await jsonResponse.json();
// Text response
const textResponse = await fetch("/api/message");
const textData = await textResponse.text();
// Blob response (for images, files)
const imageResponse = await fetch("/api/image.jpg");
const imageBlob = await imageResponse.blob();
const imageUrl = URL.createObjectURL(imageBlob);
// FormData response
const formResponse = await fetch("/api/form");
const formData = await formResponse.formData();
// ArrayBuffer (for binary data)
const binaryResponse = await fetch("/api/binary");
const buffer = await binaryResponse.arrayBuffer();
// Response properties
console.log("Status:", jsonResponse.status); // 200, 404, etc.
console.log("OK:", jsonResponse.ok); // true if 200-299
console.log("Headers:", jsonResponse.headers.get("Content-Type"));
console.log("URL:", jsonResponse.url);
}
Important: Response body can only be read once! If you need to read it multiple times, clone the response first: const clone = response.clone();
Error Handling with Fetch
Proper error handling is crucial for robust applications:
async function robustFetch(url, options = {}) {
try {
const response = await fetch(url, options);
// Handle HTTP errors
if (!response.ok) {
// Try to get error details from response
let errorMessage = `HTTP error! status: ${response.status}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorMessage;
} catch {
// Response body is not JSON
}
throw new Error(errorMessage);
}
return await response.json();
} catch (error) {
// Network errors, timeout, or thrown errors
if (error.name === "TypeError") {
console.error("Network error or CORS issue");
}
console.error("Fetch error:", error.message);
throw error;
}
}
// With timeout
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);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
throw new Error("Request timeout");
}
throw error;
}
}
Real-World Example: Complete CRUD API
// API helper class
class UserAPI {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const config = {
...options,
headers: {
"Content-Type": "application/json",
...options.headers
}
};
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `HTTP error! status: ${response.status}`);
}
// Handle 204 No Content
if (response.status === 204) {
return null;
}
return await response.json();
}
// GET all users
async getAll() {
return await this.request("/users");
}
// GET single user
async getById(id) {
return await this.request(`/users/${id}`);
}
// POST create user
async create(userData) {
return await this.request("/users", {
method: "POST",
body: JSON.stringify(userData)
});
}
// PUT update user
async update(id, userData) {
return await this.request(`/users/${id}`, {
method: "PUT",
body: JSON.stringify(userData)
});
}
// DELETE user
async delete(id) {
return await this.request(`/users/${id}`, {
method: "DELETE"
});
}
}
// Usage
const api = new UserAPI("https://api.example.com");
try {
const users = await api.getAll();
const user = await api.getById(1);
const newUser = await api.create({ name: "John", email: "john@example.com" });
await api.update(1, { name: "Jane" });
await api.delete(1);
} catch (error) {
console.error("API Error:", error.message);
}
Practice Exercise:
Task: Create a function that fetches posts from an API and handles all possible errors gracefully.
// Your task: Implement this function
async function fetchPostsWithRetry(url, maxRetries = 3) {
// 1. Try to fetch posts
// 2. If it fails, retry up to maxRetries times
// 3. Handle network errors, HTTP errors, and timeouts
// 4. Return posts or throw descriptive error
}
Solution:
async function fetchPostsWithRetry(url, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`Attempt ${attempt} of ${maxRetries}`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const posts = await response.json();
console.log(`Success! Fetched ${posts.length} posts`);
return posts;
} catch (error) {
lastError = error;
if (error.name === "AbortError") {
console.warn(`Attempt ${attempt}: Request timeout`);
} else {
console.warn(`Attempt ${attempt}: ${error.message}`);
}
// Don't wait after last attempt
if (attempt < maxRetries) {
const delay = attempt * 1000; // Exponential backoff
console.log(`Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}
Summary
In this lesson, you learned:
- Fetch API provides modern promise-based HTTP requests
- GET, POST, PUT, PATCH, DELETE methods for different operations
- Request headers and options for customization
- Response handling with .json(), .text(), .blob(), etc.
- Proper error handling for HTTP and network errors
- Fetch only rejects on network errors, not HTTP errors
- Building reusable API helper classes
Next Up: In the next lesson, we'll dive deeper into working with JSON data, including parsing, stringifying, and handling edge cases!