Advanced JavaScript (ES6+)

Fetch API

13 min Lesson 17 of 40

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!