Full Stack Learning Hub

Comprehensive guides, cheat sheets, and code examples for full stack development.

View on GitHub

JavaScript Fetch API Guide

Quick Reference Card

Concept Syntax Purpose
Basic Fetch fetch(url) Make HTTP request, returns Promise
Get JSON response.json() Parse response body as JSON
Async/Await const res = await fetch(url) Wait for promise to resolve
Error Handling try { } catch (error) { } Handle network errors
Check Status if (response.ok) { } Verify HTTP 200-299 status
POST Request fetch(url, { method: 'POST' }) Send data to server
Set Headers { headers: { 'Content-Type': 'application/json' } } Configure request metadata
Send JSON body: JSON.stringify(data) Serialize data for POST/PUT

Table of Contents

  1. What is Fetch API?
  2. Basic GET Requests
  3. Understanding Responses
  4. Error Handling
  5. POST Requests
  6. Headers and Configuration
  7. Real-World Patterns
  8. Common Pitfalls

What is Fetch API?

The Fetch API is the modern JavaScript standard for making HTTP requests. It replaces the older XMLHttpRequest and provides a cleaner, Promise-based interface for network communication.

Key Characteristics

// Fetch returns a Promise
fetch("https://api.example.com/data")
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

Why Use Fetch?


Basic GET Requests

Fetch with Promise Chain

const button = document.getElementById("btn");

button.addEventListener("click", () => {
  fetch("https://fakestoreapi.com/products/1")
    .then((response) => {
      // Response is the HTTP response object
      return response.json(); // Parse JSON from response body
    })
    .then(data => {
      // data is now a JavaScript object
      console.log(data);

      // Display in DOM
      const body = document.querySelector("body");
      const h1 = document.createElement("h1");
      h1.textContent = data.title;
      body.appendChild(h1);
    })
    .catch(error => {
      console.error("Something went wrong:", error);
    });
});
const fetchProductData = async () => {
  try {
    const response = await fetch("https://fakestoreapi.com/products/1");
    const data = await response.json();
    console.log(data);

    // Display in DOM
    const body = document.querySelector("body");
    const h1 = document.createElement("h1");
    const img = document.createElement('img');

    h1.textContent = data.title;
    img.setAttribute("src", data.image);

    body.appendChild(h1);
    body.appendChild(img);
  } catch (error) {
    console.error("Something went wrong:", error);
  }
};

fetchProductData();

Key Points:


Understanding Responses

The Response Object

When you call fetch(), you get a Response object that contains:

const response = await fetch("https://api.example.com/data");

console.log(response.ok);        // true if status 200-299
console.log(response.status);    // HTTP status code (200, 404, 500, etc.)
console.log(response.statusText); // Status text ("OK", "Not Found", etc.)
console.log(response.headers);   // Response headers
console.log(response.url);       // Final URL after redirects

Extracting Data from Response

The response body can be read in different formats:

// Parse as JSON (most common)
const data = await response.json();

// Read as plain text
const text = await response.text();

// Read as Blob (for images, files)
const blob = await response.blob();

// Read as FormData
const formData = await response.formData();

IMPORTANT: Double Await Pattern

// BAD: Only one await
const data = await fetch("https://api.com/data");
console.log(data); // Response object, NOT the actual data

// GOOD: Two awaits
const response = await fetch("https://api.com/data");
const data = await response.json(); // Now data is the actual JSON
console.log(data);

Error Handling

Two Types of Errors

Fetch API distinguishes between network errors and HTTP errors:

const fetchData = async () => {
  try {
    const response = await fetch("https://api.example.com/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) {
    // Catches both network errors AND thrown HTTP errors
    console.error("Fetch failed:", error);
  }
};

Network Errors vs HTTP Errors

// Network Error: No internet, DNS failure, CORS block
// fetch() Promise REJECTS → goes to catch block

// HTTP Error: Server responds with 404, 500, etc.
// fetch() Promise RESOLVES → check response.ok manually

Comprehensive Error Handling Pattern

const getPokemon = async (pokemonName) => {
  try {
    const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonName}`);

    // Handle HTTP errors
    if (!response.ok) {
      if (response.status === 404) {
        throw new Error(`Pokemon "${pokemonName}" not found`);
      } else if (response.status === 500) {
        throw new Error("Server error - try again later");
      } else {
        throw new Error(`HTTP error: ${response.status}`);
      }
    }

    const data = await response.json();

    return {
      name: data.name,
      type: data.types[0].type.name,
      hp: data.stats[0].base_stat,
      sprite: data.sprites.other.dream_world.front_default
    };
  } catch (error) {
    // Network error OR thrown HTTP error
    console.error("Error fetching Pokemon:", error.message);
    return null;
  }
};

// Usage
const pokemon = await getPokemon("pikachu");
if (pokemon) {
  console.log(pokemon);
}

POST Requests

Sending JSON Data

const createUser = async (userData) => {
  try {
    const response = await fetch("https://api.example.com/users", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(userData) // Convert JS object to JSON string
    });

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

    const newUser = await response.json();
    return newUser;
  } catch (error) {
    console.error("Failed to create user:", error);
  }
};

// Usage
const user = await createUser({
  name: "John Doe",
  email: "john@example.com",
  age: 30
});

Request Configuration Options

fetch(url, {
  method: "POST",           // HTTP method
  headers: {                // Request headers
    "Content-Type": "application/json",
    "Authorization": "Bearer token123"
  },
  body: JSON.stringify(data), // Request body (must be string)
  mode: "cors",             // CORS mode (cors, no-cors, same-origin)
  credentials: "include",   // Send cookies (include, same-origin, omit)
  cache: "no-cache",        // Cache mode
  redirect: "follow"        // Redirect behavior
});

Form Data POST

const uploadForm = async (formElement) => {
  const formData = new FormData(formElement);

  try {
    const response = await fetch("https://api.example.com/upload", {
      method: "POST",
      body: formData // FormData automatically sets correct Content-Type
    });

    if (!response.ok) {
      throw new Error("Upload failed");
    }

    const result = await response.json();
    return result;
  } catch (error) {
    console.error("Error uploading form:", error);
  }
};

Headers and Configuration

Setting Request Headers

const fetchWithAuth = async () => {
  const token = localStorage.getItem("authToken");

  const response = await fetch("https://api.example.com/protected", {
    method: "GET",
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/json",
      "Accept": "application/json"
    }
  });

  return await response.json();
};

Reading Response Headers

const response = await fetch("https://api.example.com/data");

// Get specific header
const contentType = response.headers.get("Content-Type");
console.log(contentType); // "application/json"

// Check if header exists
if (response.headers.has("X-Custom-Header")) {
  console.log("Custom header present");
}

// Iterate all headers
for (const [key, value] of response.headers) {
  console.log(`${key}: ${value}`);
}

Real-World Patterns

Pattern 1: Pokemon Fetcher with Display

const cardContainer = document.querySelector(".cardContainer");

const getPokemon = async (pokemon) => {
  try {
    const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`);

    if (!response.ok) {
      throw new Error(`Pokemon not found: ${pokemon}`);
    }

    const data = await response.json();

    const pokeData = {
      name: data.name,
      type: data.types[0].type.name,
      hp: data.stats[0].base_stat,
      height: data.height,
      weight: data.weight,
      attack: data.stats[1].base_stat,
      sprite: data.sprites.other.dream_world.front_default
    };

    return pokeData;
  } catch (error) {
    console.error("Error:", error.message);
    return null;
  }
};

const displayPokemon = (pokedata) => {
  if (!pokedata) return;

  cardContainer.innerHTML = '';
  const myCard = document.createElement('div');
  myCard.className = 'my-card';

  myCard.innerHTML = `
    <img class='pokePic' src='${pokedata.sprite}'>
    <div class="info">
      <h3>${pokedata.name}</h3>
      <p>Type: ${pokedata.type}</p>
      <h3>Stats:</h3>
      <p>HP: ${pokedata.hp} - ATK: ${pokedata.attack}</p>
      <p>Height: ${pokedata.height} - Weight: ${pokedata.weight}</p>
    </div>
  `;

  cardContainer.appendChild(myCard);
};

// Event handler
const myForm = document.querySelector("form");
const myInput = document.getElementById("searchBar");

myForm.addEventListener("submit", async (event) => {
  event.preventDefault();
  const pokemon = myInput.value.toLowerCase().trim();

  if (!pokemon) return;

  const pokeData = await getPokemon(pokemon);
  displayPokemon(pokeData);
  myInput.value = '';
});

Pattern 2: LocalStorage Integration

// Save fetched data to localStorage
const catchPokemon = (pokeData) => {
  const isCaught = Math.random() < 0.5; // 50% catch rate

  if (isCaught) {
    // Get existing team from localStorage
    const myTeam = JSON.parse(localStorage.getItem("myTeam")) || [];

    // Add new pokemon to team
    myTeam.push(pokeData);

    // Save back to localStorage
    localStorage.setItem("myTeam", JSON.stringify(myTeam));

    alert(`Success! You caught ${pokeData.name}`);
  } else {
    alert(`You failed to catch ${pokeData.name}. Try again!`);
  }
};

// Load from localStorage on page load
const loadMyTeam = () => {
  const myTeam = JSON.parse(localStorage.getItem("myTeam")) || [];

  if (myTeam.length === 0) {
    teamContainer.innerHTML = '<p class="empty-message">No Pokemon caught yet!</p>';
    return;
  }

  myTeam.forEach((pokemon) => {
    const pokemonCard = document.createElement('div');
    pokemonCard.className = 'pokemon-card';

    pokemonCard.innerHTML = `
      <img class="pokemon-pic" src="${pokemon.sprite}" alt="${pokemon.name}">
      <div class="pokemon-info">
        <h3>${pokemon.name}</h3>
        <p>${pokemon.type}</p>
        <p>Height: ${pokemon.height}</p>
        <p>Weight: ${pokemon.weight}</p>
      </div>
      <div class="pokemon-stats">
        <p>HP: ${pokemon.hp}</p>
        <p>Attack: ${pokemon.attack}</p>
      </div>
    `;

    teamContainer.appendChild(pokemonCard);
  });
};

window.addEventListener('load', loadMyTeam);

Pattern 3: Loading States

const fetchDataWithLoading = async () => {
  const statusElement = document.getElementById("status");

  try {
    // Show loading state
    statusElement.textContent = "Loading...";
    statusElement.className = "loading";

    const response = await fetch("https://api.example.com/data");

    if (!response.ok) {
      throw new Error("Failed to fetch data");
    }

    const data = await response.json();

    // Show success state
    statusElement.textContent = "Data loaded successfully!";
    statusElement.className = "success";

    return data;
  } catch (error) {
    // Show error state
    statusElement.textContent = `Error: ${error.message}`;
    statusElement.className = "error";
    return null;
  }
};

Pattern 4: Retry Logic

const fetchWithRetry = async (url, maxRetries = 3) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url);

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

      return await response.json();
    } catch (error) {
      console.warn(`Attempt ${attempt} failed:`, error.message);

      if (attempt === maxRetries) {
        throw new Error(`Failed after ${maxRetries} attempts`);
      }

      // Wait before retry (exponential backoff)
      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
    }
  }
};

// Usage
try {
  const data = await fetchWithRetry("https://api.example.com/data");
  console.log(data);
} catch (error) {
  console.error("All retry attempts failed:", error);
}

Common Pitfalls

Pitfall 1: Forgetting to Check response.ok

// BAD: Doesn't handle HTTP errors
const data = await fetch(url).then(res => res.json());

// GOOD: Always check response.ok
const response = await fetch(url);
if (!response.ok) {
  throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();

Pitfall 2: Missing Second Await

// BAD: Only awaits fetch, not .json()
const data = await fetch(url).then(res => res.json());
// If .json() fails, the promise rejection is unhandled

// GOOD: Await both operations
const response = await fetch(url);
const data = await response.json();

Pitfall 3: Not Handling Network Errors

// BAD: No error handling
const data = await fetch(url).then(res => res.json());

// GOOD: Wrap in try-catch
try {
  const response = await fetch(url);
  const data = await response.json();
} catch (error) {
  console.error("Network or parse error:", error);
}

Pitfall 4: Forgetting JSON.stringify for POST

// BAD: Sending object directly
fetch(url, {
  method: "POST",
  body: { name: "John" } // Won't work!
});

// GOOD: Stringify the object
fetch(url, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "John" })
});

Pitfall 5: CORS Issues

// If you get CORS errors:
// - Server must send proper Access-Control-Allow-Origin headers
// - You cannot fix CORS from the client side
// - Use mode: 'no-cors' only if you don't need the response

// Limited access (no response body)
fetch(url, { mode: 'no-cors' });

// Proper solution: Configure server CORS headers
// Express example:
// app.use(cors({ origin: 'https://your-site.com' }));

HTTP Status Codes Reference

Status Code Meaning Action
200 OK Success - process data
201 Created Resource created successfully
204 No Content Success - no response body
400 Bad Request Invalid request data
401 Unauthorized Authentication required
403 Forbidden Access denied
404 Not Found Resource doesn’t exist
500 Internal Server Error Server-side error
503 Service Unavailable Server temporarily down

Summary

Key Takeaways:

  1. Fetch returns a Promise - use async/await or .then()
  2. Two awaits needed - one for fetch, one for .json()
  3. Check response.ok - fetch doesn’t reject on HTTP errors
  4. Use try…catch - handle both network and parse errors
  5. JSON.stringify for POST - convert objects to JSON strings
  6. Set Content-Type header - tell server what you’re sending

Best Practice Pattern:

const fetchData = async (url) => {
  try {
    const response = await fetch(url);

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

    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Fetch failed:", error);
    return null;
  }
};

See Also