Ad – 728Γ—90
🌐 DOM & Browser

JavaScript Fetch API – Making HTTP Requests

The Fetch API is the modern, Promise-based way to make HTTP requests from the browser. It replaces the verbose XMLHttpRequest with a clean, readable interface that integrates naturally with async/await. From loading JSON to uploading files, Fetch handles virtually every HTTP scenario.

⏱️ 20 min read 🎯 Intermediate πŸ“… Updated 2026

Fetch Basics

fetch(url) returns a Promise that resolves to a Response object. The response body must be explicitly parsed β€” the most common method is .json():

JavaScript
// Basic GET with .then() chain
fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then(response => response.json())  // parse JSON body
  .then(data => console.log(data))
  .catch(error => console.error('Network error:', error));

// The same with async/await (cleaner)
async function getPost(id) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  const post = await response.json();
  console.log(post);
  return post;
}

getPost(1);
β–Ά Output
{ userId: 1, id: 1, title: "sunt aut facere repellat...", body: "quia et suscipit..." }

The Response Object

The Response object has important properties and multiple body-reading methods:

JavaScript
async function inspectResponse() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');

  // Status info
  console.log(response.status);     // 200, 404, 500, etc.
  console.log(response.statusText); // "OK", "Not Found", etc.
  console.log(response.ok);         // true for 200–299
  console.log(response.url);        // final URL (after redirects)
  console.log(response.redirected); // true if URL changed

  // Headers
  console.log(response.headers.get('Content-Type'));
  // "application/json; charset=utf-8"

  // Body parsing methods (each returns a Promise, use only ONE per response)
  const json   = await response.json();    // parses JSON
  // const text = await response.text();  // raw string
  // const blob = await response.blob();  // binary data (images, files)
  // const buf  = await response.arrayBuffer(); // binary buffer
  // const form = await response.formData();    // multipart form data
}
⚠️
Response body can only be read once

Once you call response.json(), response.text(), or any body method, the body stream is consumed. Calling a second body method throws an error. If you need to read the body multiple times, clone the response first: const clone = response.clone().

Handling HTTP Errors

Fetch only rejects (goes to catch) on network failures (offline, DNS error). HTTP error codes like 404 or 500 resolve successfully β€” you must check response.ok:

JavaScript
async function fetchUser(id) {
  try {
    const response = await fetch(`/api/users/${id}`);

    // Must check response.ok for HTTP errors
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
    }

    const user = await response.json();
    return user;

  } catch (error) {
    if (error.name === 'TypeError') {
      // Network failure (no internet, DNS fail, CORS block)
      console.error('Network error β€” are you offline?');
    } else {
      // HTTP error or JSON parse error
      console.error('Request failed:', error.message);
    }
    return null;
  }
}

// Usage
const user = await fetchUser(42);
if (user) renderUser(user);

POST Request with JSON Body

JavaScript
async function createPost(postData) {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer my-token-here'
    },
    body: JSON.stringify(postData)
  });

  if (!response.ok) throw new Error(`Failed: ${response.status}`);

  const created = await response.json();
  console.log('Created post with ID:', created.id);
  return created;
}

createPost({
  title: 'Hello Fetch',
  body: 'Learning the Fetch API',
  userId: 1
});

// PUT, PATCH, DELETE follow the same pattern
async function deletePost(id) {
  const response = await fetch(`/api/posts/${id}`, { method: 'DELETE' });
  if (response.status !== 204 && !response.ok) {
    throw new Error('Delete failed');
  }
  console.log('Deleted post', id);
}
β–Ά Output
Created post with ID: 101

Fetch Options Reference

OptionTypeDescription
methodstring"GET" (default), "POST", "PUT", "PATCH", "DELETE", "HEAD"
headersobject / HeadersRequest headers, e.g. Content-Type, Authorization
bodystring / FormData / Blob / URLSearchParamsRequest payload; not allowed for GET/HEAD
modestring"cors" (default), "no-cors", "same-origin"
credentialsstring"same-origin" (default), "include" (send cookies cross-origin), "omit"
cachestring"default", "no-store", "reload", "force-cache"
signalAbortSignalFor cancellation via AbortController

Cancellation with AbortController

JavaScript
let currentController = null;

async function search(query) {
  // Cancel any in-flight request from previous call
  if (currentController) currentController.abort();

  currentController = new AbortController();
  const { signal } = currentController;

  try {
    const response = await fetch(
      `https://api.example.com/search?q=${encodeURIComponent(query)}`,
      { signal }
    );

    if (!response.ok) throw new Error(`HTTP ${response.status}`);

    const results = await response.json();
    displayResults(results);

  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Request was cancelled (new search started)');
    } else {
      console.error('Search failed:', error.message);
    }
  }
}

// Timeout pattern using AbortController
async function fetchWithTimeout(url, ms = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), ms);

  try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(timeoutId);
    return await response.json();
  } catch (e) {
    if (e.name === 'AbortError') throw new Error(`Request timed out after ${ms}ms`);
    throw e;
  }
}

Fetch vs XMLHttpRequest

Featurefetch()XMLHttpRequest
API stylePromise-basedCallback-based (event listeners)
async/await supportNativeRequires wrapping in a Promise
Upload progressNot directly (use XHR or streams)xhr.upload.onprogress
Request cancellationAbortControllerxhr.abort()
HTTP error detectionManual (response.ok)Manual (status check)
Streaming responsesReadableStream via response.bodyLimited
Code verbosityConciseVerbose
ℹ️
CORS in a nutshell

Cross-Origin Resource Sharing (CORS) is a browser security policy that blocks requests to a different origin (scheme + domain + port) unless the server responds with Access-Control-Allow-Origin headers. Fetch respects CORS automatically β€” if the server doesn't allow your origin, the browser blocks the response and fetch rejects with a TypeError.

πŸ‹οΈ Practical Exercise

Build a user profile card loader:

  1. On page load, fetch all users from https://jsonplaceholder.typicode.com/users.
  2. Render each user as a card with name, email, and company name.
  3. Add a "Load Posts" button on each card that fetches /posts?userId={id} and shows the post titles in a dropdown list.
  4. Handle errors: show a friendly message if the fetch fails.

πŸ”₯ Challenge Exercise

Build a debounced live search that queries https://jsonplaceholder.typicode.com/posts?title_like={query} as the user types. Cancel in-flight requests using AbortController. Show a loading spinner during the request, display results as a list, and show "No results found" when the array is empty. Debounce input events by 300 ms.

Summary

πŸ“‹ Summary

  • fetch(url) returns a Promise resolving to a Response object.
  • Always check response.ok β€” Fetch only rejects on network failures, not HTTP errors.
  • Parse the body with .json(), .text(), or .blob() β€” each consumes the body stream.
  • Set method, headers, and body in the options object for POST/PUT/PATCH.
  • Use AbortController to cancel in-flight requests or implement timeouts.
  • CORS is enforced by the browser β€” the server must send appropriate headers for cross-origin requests.

Interview Questions

  • Why doesn't fetch reject on a 404 response? How do you handle it?
  • How would you cancel a fetch request initiated 500 ms ago?
  • What is the difference between credentials: 'include' and the default?
  • How do you implement a request timeout with fetch?
  • When would you still prefer XMLHttpRequest over fetch?

Frequently Asked Questions

How do I send cookies with a cross-origin fetch request? +

Pass credentials: 'include' in the options object. The server must also respond with Access-Control-Allow-Credentials: true and a specific (non-wildcard) Access-Control-Allow-Origin header. Without both, the browser will block the response.

How do I upload a file with fetch? +

Use a FormData object as the body. Append the file with formData.append('file', fileInput.files[0]) and pass the FormData to fetch without setting Content-Type β€” the browser sets it automatically with the correct multipart boundary. Do not set Content-Type manually or you'll break the boundary.

Can fetch be used in Node.js? +

Yes. The global fetch function was added natively to Node.js in version 18 (stable in v21+). For older Node versions, you can install the node-fetch package or use Axios. In modern full-stack projects, the same fetch code runs in both browser and Node environments.

Ad – 336Γ—280