Fetch API in JavaScript

The Fetch API is a built-in browser tool that lets you request data from a server — or send data to one — without refreshing the page. It is how modern web apps load things dynamically, like showing new posts in a feed or checking a weather update.

Think of it like sending a text message. You send a request, and at some point you get a reply. You do not have to stop everything and wait — your app keeps running, and when the response arrives, you handle it.

Before Fetch, developers used something called XMLHttpRequest — a much older and clunkier way to do the same thing. Fetch is simpler, cleaner, and Promise-based, which means it works perfectly with async/await.

javascript

// Basic fetch call
fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error("Error:", error));

The fetch() function returns a Promise. That Promise resolves to a Response object — not the actual data yet. You still need to read the response body, which we will cover next.

A GET request is used to fetch data from a server. It is the most common type of HTTP request. When you type a URL into your browser, that is a GET request.

By default, fetch() makes a GET request — you do not need to specify the method. Just pass the URL:

javascript

fetch("https://jsonplaceholder.typicode.com/users/1")
  .then(response => response.json())
  .then(user => {
    console.log(user.name);    // Leanne Graham
    console.log(user.email);   // Sincere@april.biz
  })
  .catch(error => console.error("Request failed:", error));

The fetch call happens in two steps:

  • Step 1: The Promise resolves with a Response object when the server replies
  • Step 2: You call .json() on the response to read the actual data (this also returns a Promise)

javascript

// Cleaner with async/await
async function getUser() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
  const user = await response.json();
  console.log(user.name);  // Leanne Graham
}

getUser();
Note: fetch() only rejects when there is a network problem (like no internet). A 404 or 500 response does NOT automatically throw an error. You need to check response.ok yourself.

When fetch() gets a reply from the server, it gives you a Response object first. This object has metadata like the status code and headers. The actual data is inside the response body, and you need to read it separately.

The Response object has several methods to read the body, depending on what type of data you expect:

  • response.json() — use when the server returns JSON data (most common)
  • response.text() — use when the server returns plain text or HTML
  • response.blob() — use for binary data like images or files

javascript

// Reading JSON response
async function getPosts() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  const data = await response.json();
  console.log(data.title); // sunt aut facere repellat...
  console.log(data.body);  // the full body text
}

// Reading a text response
async function getHTML() {
  const response = await fetch("/about.html");
  const html = await response.text();
  console.log(html.substring(0, 100)); // first 100 chars of HTML
}

getPosts();

The Response object also has useful properties you can inspect:

javascript

async function inspect() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");

  console.log(response.status);     // 200
  console.log(response.ok);         // true (status 200-299)
  console.log(response.statusText); // "OK"
  console.log(response.url);        // the request URL

  const data = await response.json();
  console.log(data.id); // 1
}

inspect();
Important: You can only read the response body once. If you call .json() on a response, you cannot call .text() on it afterward. It will throw an error saying the body is already used.

This is one of the most important things to understand about Fetch: it does not throw an error for bad HTTP responses like 404 (Not Found) or 500 (Server Error). It only rejects the Promise when there is a network failure — like no internet connection.

So a 404 response will go into your .then() block, not your .catch() block. You have to check response.ok manually.

javascript

async function getPost(id) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);

    // Check if the request was successful
    if (!response.ok) {
      throw new Error(`Request failed: ${response.status}`);
    }

    const data = await response.json();
    console.log(data.title);
  } catch (error) {
    console.error("Error:", error.message);
  }
}

getPost(1);    // works fine
getPost(9999); // Error: Request failed: 404

A reusable pattern for this is to create a helper function that handles the check every time:

javascript

async function safeFetch(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }
  return response.json();
}

// Now use it anywhere
safeFetch("https://jsonplaceholder.typicode.com/posts/1")
  .then(data => console.log(data.title))
  .catch(err => console.error(err.message));
Rule of thumb: Always check response.ok before reading the body. It is true for status codes 200–299 and false for anything else.

A POST request is used to send data to a server — like submitting a form, creating a new user, or saving a blog post. Unlike a GET request, you need to configure the request manually by passing an options object as the second argument to fetch().

javascript

async function createPost() {
  const newPost = {
    title: "My First Post",
    body: "This is the content of my post.",
    userId: 1
  };

  const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(newPost)
  });

  const data = await response.json();
  console.log(data.id);    // 101 (the new post ID)
  console.log(data.title); // My First Post
}

createPost();

Three things to remember when making a POST request:

  • Set method: "POST" — fetch defaults to GET
  • Set Content-Type: "application/json" in headers so the server knows what format the data is in
  • Use JSON.stringify() to convert your object to a JSON string for the body

javascript

// You can also use PUT to update and DELETE to remove
async function deletePost(id) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
    method: "DELETE"
  });

  if (response.ok) {
    console.log(`Post ${id} deleted successfully`);
  }
}

deletePost(1);
Remember: Always set the Content-Type header when sending JSON. Without it, many servers will not know how to read your request body.

Fetch and async/await are a perfect combination. Fetch gives you the ability to talk to servers. Async/await makes that code clean and easy to follow. Together, they cover most of what you need for working with APIs in a real project.

javascript

// A complete real-world pattern
async function getUserPosts(userId) {
  try {
    // Step 1: Get the user
    const userRes = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
    if (!userRes.ok) throw new Error("User not found");
    const user = await userRes.json();

    // Step 2: Get their posts
    const postsRes = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`);
    if (!postsRes.ok) throw new Error("Posts not found");
    const posts = await postsRes.json();

    console.log(`${user.name} has ${posts.length} posts`);
    // Leanne Graham has 10 posts
  } catch (error) {
    console.error("Failed:", error.message);
  }
}

getUserPosts(1);

When multiple requests do not depend on each other, run them in parallel using Promise.all() to save time:

javascript

async function loadDashboard() {
  try {
    const [usersRes, postsRes] = await Promise.all([
      fetch("https://jsonplaceholder.typicode.com/users"),
      fetch("https://jsonplaceholder.typicode.com/posts")
    ]);

    const users = await usersRes.json();
    const posts = await postsRes.json();

    console.log("Users:", users.length);  // 10
    console.log("Posts:", posts.length);  // 100
  } catch (error) {
    console.error("Failed to load:", error.message);
  }
}

loadDashboard();
Tip: In a real app, you would show a loading state while fetch runs, then show the data when it arrives, and show an error message if something goes wrong. This pattern covers all three cases.
  • fetch() is a built-in browser function to make HTTP requests
  • It returns a Promise that resolves to a Response object — not the data directly
  • Call response.json() or response.text() to read the response body
  • fetch() only rejects on network failure — always check response.ok for HTTP errors
  • For GET requests, just pass a URL. No extra configuration needed.
  • For POST requests, provide method, headers, and a JSON-stringified body
  • Fetch works great with async/await for clean, readable code
  • Use Promise.all() to run multiple fetch calls in parallel

What's next? Now that you know how to fetch data, let's organize JavaScript into reusable files in the next tutorial.

  • What does the Fetch API do, and how is it different from XMLHttpRequest?
  • What does fetch() return?
  • Why do you need to call .json() on the response? Why is it not automatic?
  • Does fetch() throw an error for a 404 response? How do you handle that?
  • What is response.ok and what values does it have?
  • How do you send a POST request with fetch?
  • What is the role of the Content-Type header in a POST request?
  • How would you make two fetch requests at the same time?
  • Can you use the response body more than once?
  • How do you cancel a fetch request?
Async & Await
Modules