Fetch API

Learn how to use the Fetch API in JavaScript to make HTTP requests, handle responses, and manage errors.

Loading...
Fetch API

What is the Fetch API?

In JavaScript, Fetch API is a modern way to make network requests, like getting data from a server.

Before Fetch, developers used XMLHttpRequest (XHR) which was more complex and harder to use.

Fetch is a more powerful and flexible replacement for the older XMLHttpRequest.

Fetch is a browser API for making HTTP requests and returns a Promise that resolves to a Response object.

You can think of it as a way for your web page to talk to servers and get data without refreshing the entire page.

It’s built into all modern browsers and is used for easily requesting resources such as JSON, text, images, or any other data from a URL.


Basic Syntax

fetch(url, options)
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error("Error:", error));

Here,

  • url (string): The URL to request.
  • options (object): Optional settings like method, headers, body, etc.

How Fetch Works

  • Call fetch() with a URL (and optional options).
  • It returns a Promise that resolves to a Response object once the request completes.
  • Check if the response is successful (response.ok).
  • Extract the response data using methods like .json(), .text(), or .blob().
  • Handle errors if the network fails or the server returns an error status.

Making Your First Request

Simple GET Request

A GET request simply asks the server for some data.

// Fetch data from a public API
fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then((response) => response.json()) // Convert response to JSON
  .then((data) => {
    console.log(data); // Work with the actual data
  })
  .catch((error) => {
    console.error("Something went wrong:", error);
  });

Here,

  • fetch(...) sends a request to the server.
  • .then(response => response.json()) converts the raw response into a usable JavaScript object.
  • The second .then(...) lets you use the actual data.
  • .catch(...) handles any errors, like network issues.

The same thing, but with async/await, as it is much cleaner and easier to read.

async function fetchPost() {
  try {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/posts/1"
    );
    const data = await response.json(); // Parse JSON
    console.log(data); // Log the result
  } catch (error) {
    console.error("Error fetching data:", error);
  }
}
 
fetchPost();

It looks like normal code and easier to debug and handle errors with try/catch.


Understanding the Response Object

When you make a fetch request, it doesn't directly give you the data, it first gives you a Response object.

This object holds important details about the response from the server, like status codes, headers, and the final URL.

async function examineResponse() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
 
  console.log(response.status); // 200
  console.log(response.statusText); // "OK"
  console.log(response.ok); // true (status 200-299)
  console.log(response.headers); // all response headers
  console.log(response.url); // the final URL of the request
 
  const data = await response.json(); // Convert to JSON and finally, get the real data
}

Here,

  • response.status: The HTTP status code (e.g., 200, 404, 500).
  • response.statusText: A short description (e.g., "OK", "Not Found").
  • response.ok: true if the status code is between 200 and 299, success!
  • response.headers: Metadata sent by the server (like content type, cache info).
  • response.url: The actual URL used for the request (could include redirects).

Once you check the response, you usually parse the body using .json(), .text(), or .blob() depending on the type of data.

Note: Always check response.ok before using the data, just in case the request failed silently.


Different Ways to Parse Response Data

When you use fetch, the response body isn’t immediately usable. You need to parse it based on what type of data you're expecting.

Here's how to do that for common formats:

JSON Data

Use .json() when you're expecting a JSON response (like most APIs return).

const response = await fetch("/api/users");
const jsonData = await response.json(); // Converts the body into a JavaScript object

Use this when the server responds with application/json.

Text Data

Use .text() when the response is plain text (like a message or raw HTML).

const response = await fetch("/api/message");
const textData = await response.text(); // Gets raw string data

Use this when the server sends simple text, not structured JSON.

Blob Data (for files)

Use .blob() when you're downloading things like images, PDFs, or videos.

const response = await fetch("/api/image");
const blobData = await response.blob(); // Creates a Blob object for file-like data

Use this when the response contains binary file data.

Note: You can only use one parser method per response, once you call .json(), .text(), or .blob(), you can’t read it again.


Request Options in Fetch

The fetch() function can take two arguments:

  • The URL you want to request
  • An options object that lets you customize the request (method, headers, body, etc.)

Basic structure:

const options = {
  method: "POST", // Type of request: GET, POST, PUT, DELETE, etc.
  headers: {
    "Content-Type": "application/json", // Tells the server you're sending JSON
    Authorization: "Bearer token123", // Auth token (if required)
  },
  body: JSON.stringify(data), // Convert JS object to JSON string
  credentials: "include", // Sends cookies along with the request
  cache: "no-cache", // Forces fetch to get fresh data, ignoring cached versions
  redirect: "follow", // Auto-follow redirects (default)
};
 
fetch("https://api.example.com/data", options);

Here,

  • method: HTTP method (GET, POST, PUT, DELETE, etc.)
  • headers: Key-value pairs to describe the request (like Content-Type)
  • body: Data you want to send (use with POST or PUT) |
  • credentials: Include cookies or auth info (include, same-origin, or omit) |
  • cache: Controls caching (default, no-cache, reload, etc.) |
  • redirect: Follow redirects automatically (follow), or block them (error) |

Example:

async function submitData(data) {
  const response = await fetch("https://api.example.com/submit", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer your_token_here",
    },
    body: JSON.stringify(data),
    credentials: "include",
    cache: "no-cache",
    redirect: "follow",
  });
 
  const result = await response.json();
  console.log(result);
}

HTTP Methods with Fetch

The fetch() API supports all HTTP methods: GET, POST, PUT, DELETE, etc. You just need to set the method in the options object.

GET Request (Read Data)

The default method for fetch() is GET. You don’t have to specify it unless you want to be explicit.

This is used when you want to fetch (read) data from the server.

// These are equivalent
const response1 = await fetch("/api/users");
const response2 = await fetch("/api/users", { method: "GET" });

Here,

  • fetch("/api/users") sends a GET request by default.
  • { method: "GET" } is optional but can be added for clarity.
  • This type of request retrieves data from the server.
  • You usually follow it with await response.json() to access the actual data.

POST Request (Create Data)

The POST method is used when you want to send new data to the server, like adding a new item or record.

async function createUser(userData) {
  try {
    const response = await fetch("/api/users", {
      method: "POST",
      headers: {
        "Content-Type": "application/json", // Tell the server you're sending JSON
      },
      body: JSON.stringify(userData), // Convert JS object to JSON string
    });
 
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
 
    const result = await response.json();
    return result;
  } catch (error) {
    console.error("Error creating user:", error);
  }
}
 
// Usage
const newUser = { name: "John", email: "john@example.com" };
createUser(newUser);

Here,

  • POST is used to create a new user on the server.
  • Content-Type: application/json tells the server you're sending JSON data.
  • userData is converted to JSON using JSON.stringify.
  • response.ok checks if the response is okay.
  • Returns the result as JSON if successful.
  • Errors are caught and logged in catch.

PUT Request (Update Data)

The PUT method is used when you want to update an existing item.

async function updateUser(userId, userData) {
  const response = await fetch(`/api/users/${userId}`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(userData),
  });
 
  return response.json();
}

Here,

  • PUT is used to update an existing user.
  • It targets a specific user using their ID in the URL.
  • Sends updated data in JSON format in the request body.
  • Returns the updated user object as a JSON response.

DELETE Request (Remove Data)

The DELETE method is used when you want to remove data from the server.

async function deleteUser(userId) {
  const response = await fetch(`/api/users/${userId}`, {
    method: "DELETE",
  });
 
  if (response.ok) {
    console.log("User deleted successfully");
  }
}

Here,

  • DELETE is used to remove a user based on their ID.
  • Sends a request to the server using method: "DELETE".
  • Checks if the response was successful (response.ok).
  • Logs a success message if the deletion worked.

Working with Headers in Fetch API

Headers are used to send metadata, like content type, authorization tokens, or custom data.

Reading Response Headers

When you make a fetch() request, the server might send back headers along with the response. You can inspect them using the response.headers object.

async function checkHeaders() {
  const response = await fetch("/api/data");
 
  // Get a specific header
  // get() reads the value of a header
  const contentType = response.headers.get("Content-Type");
  console.log("Content-Type:", contentType);
 
  // Check if a custom header is present
  // has() checks if a header exists
  if (response.headers.has("X-Custom-Header")) {
    console.log("Custom header found");
  }
 
  // Loop through all headers with for...of
  for (const [key, value] of response.headers) {
    console.log(`${key}: ${value}`);
  }
}

Setting Request Headers

You often need to send headers when making a request, like when you're sending JSON or adding auth tokens.

const response = await fetch("/api/protected", {
  headers: {
    Authorization: "Bearer " + token, // Auth token
    "X-API-Key": "your-api-key", // Custom API key
    "Content-Type": "application/json", // Tells server the body is JSON
  },
});

Why this matters:

  • Authorization is often needed to access protected routes.
  • Content-Type: application/json tells the server you’re sending JSON.
  • Custom headers (like "X-API-Key") can be used to identify apps or control access.

Error Handling with Fetch

Unlike some older APIs, fetch() doesn’t automatically throw an error for HTTP failures like 404 or 500. It only throws on network failures (like no internet).

Using fetch() without proper error handling can lead to silent failures and hard-to-debug issues. So you need to manually check the response and handle errors properly using try/catch.

Basic Error Handling

async function fetchWithErrorHandling() {
  try {
    const response = await fetch("/api/data");
 
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
 
    const data = await response.json();
    return data;
  } catch (error) {
    if (error.name === "TypeError") {
      // Likely a network issue
      console.error("Network error:", error.message);
    } else {
      // Any other error
      console.error("Error:", error.message);
    }
  }
}

Here,

  • fetch("/api/data") sends the request.
  • if (!response.ok) checks if the status is not in the 200–299 range.
  • throw new Error(...) lets you handle bad HTTP responses.
  • The catch block catches:
    • TypeError: likely a network failure or blocked request.
    • Other errors: like a manual throw.

Comprehensive Error Handling

Sometimes, APIs send back error messages in JSON format (like { "message": "User not found" }) when something goes wrong.

To handle these kinds of errors properly, you can write a more powerful error-handling function like this:

async function robustFetch(url, options = {}) {
  try {
    const response = await fetch(url, options);
 
    // Handle HTTP errors
    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      throw new Error(
        errorData.message || `HTTP ${response.status}: ${response.statusText}`
      );
    }
 
    return await response.json();
  } catch (error) {
    if (error.name === "TypeError") {
      // Handle network issues
      throw new Error("Network connection failed");
    }
    throw error; // Re-throw for caller to handle
  }
}

Here,

  • async function robustFetch(url, options = {}) {:
    • Defines an async function named robustFetch.
    • Takes a url and an optional options object (like method, headers, body).
    • It's a wrapper around the regular fetch() function, but smarter.
  • const response = await fetch(url, options);
    • await fetch(...) sends the request and waits for the response.
    • The result is stored in response.
  • if (!response.ok) {...}
    • response.ok is true for 2xx status codes (like 200, 201).
    • If the status is not ok (like 404, 500):
    • Try to parse the error response as JSON.
    • If the API sends { "message": "Something went wrong" }, you use that.
    • If parsing fails, fallback to a generic message like "HTTP 404: Not Found".
    • Then you throw a custom Error.
  • return await response.json(); If the response was successful, parse and return the JSON data.
  • } catch (error) {...}
    • If there’s a network problem (like no internet or DNS issue), fetch throws a TypeError.
    • You catch that and throw a clearer message: "Network connection failed".
    • For any other kind of error, you re-throw it so the caller can handle it.

Canceling a Fetch Request

Sometimes you may want to cancel a fetch request if it takes too long or the user navigates away.

You can do this with AbortController.

const controller = new AbortController();
const signal = controller.signal;
 
fetch("https://jsonplaceholder.typicode.com/posts", { signal })
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => {
    if (error.name === "AbortError") {
      console.log("Fetch aborted");
    } else {
      console.error("Fetch error:", error);
    }
  });
 
// Abort the request after 2 seconds
setTimeout(() => controller.abort(), 2000);

Here,

  • AbortController is a built-in browser API.
  • It lets you cancel DOM requests like fetch.
  • signal is used to link the controller to the request.
  • You make a fetch() call and pass { signal } to associate it with the controller.
  • If the request is aborted, .catch() will run.
  • You check if error.name === "AbortError" to handle it differently from other fetch errors.
  • setTimeout(() => controller.abort(), 2000); cancels the fetch request after 2 seconds.
  • The .catch() block will run and log "Fetch aborted".

Common Mistakes to Avoid

  • Not checking response.ok: Fetch doesn't throw errors for HTTP error status codes.
  • Forgetting to parse the response: Always call .json(), .text(), etc.
  • Not handling network errors: Always use try/catch or .catch().
  • Setting wrong Content-Type: Don't set Content-Type for FormData.
  • Not handling loading states: Show loading indicators in your UI.

Best Practices for Using Fetch API

1. Always handle errors

When you make a request, things can go wrong, network issues, server errors, or invalid responses. Without error handling, your app can crash silently or behave unpredictably.

// Good: Use try/catch and check response status
try {
  const response = await fetch("/api/data");
  if (!response.ok) throw new Error("Request failed");
  const data = await response.json();
} catch (error) {
  console.error("Error:", error);
}
 
// Bad: No error handling, your app might crash or misbehave
const response = await fetch("/api/data");
const data = await response.json(); // If this fails, there is no catch

2. Check response status

fetch does not throw errors for HTTP status like 404 or 500. You need to check response.ok manually.

// Good: Throw an error if status is not OK (200-299)
if (!response.ok) {
  throw new Error(`HTTP error! status: ${response.status}`);
}
 
// Bad: Ignoring status might lead to wrong data or silent failures
const response = await fetch("/api/data");

3. Set appropriate headers

Headers tell the server what kind of data you are sending.

// For sending JSON data
fetch("/api/data", {
  method: "POST",
  headers: {
    "Content-Type": "application/json", // Important for JSON payloads
  },
  body: JSON.stringify(data),
});
 
// For sending form data (files, etc.) with FormData
const formData = new FormData();
fetch("/api/upload", {
  method: "POST",
  body: formData, // DO NOT set Content-Type here, the browser sets it automatically
});

4. Use async/await for readability

async/await makes asynchronous code easier to read and maintain than chaining .then().

// Good: Clear and easy to understand
async function fetchData() {
  try {
    const response = await fetch("/api/data");
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Error:", error);
  }
}
 
// Less readable: Promises chaining with .then()
function fetchData() {
  return fetch("/api/data")
    .then((response) => response.json())
    .then((data) => data)
    .catch((error) => console.error("Error:", error));
}

Support my work!