All posts
Tutorials & How-To

JavaScript Fetch API: send requests, handle errors, cancel calls

Manaal Khan19 June 2026 at 1:27 am8 min read
JavaScript Fetch API: send requests, handle errors, cancel calls

Key Takeaways

JavaScript Fetch API: send requests, handle errors, cancel calls
Source:
  • Fetch returns a Promise that resolves when headers arrive, not when the request succeeds, so you must check response.ok manually
  • Use AbortController to cancel slow or abandoned requests and avoid memory leaks
  • Fetch is built into browsers and Node.js 18+, so you can skip external libraries for most HTTP work

The JavaScript Fetch API is the standard way to make HTTP requests in browsers and Node.js 18+. It replaced XMLHttpRequest with a cleaner, Promise-based interface. You call fetch() with a URL, get a Promise back, and chain .then() or use async/await. No jQuery, no external libraries.

This tutorial covers GET and POST requests, correct error handling (which trips up most developers), request cancellation with AbortController, and how Fetch compares to Ajax and Axios.

What does fetch() actually return?

fetch() returns a Promise that resolves as soon as the server sends back headers. This is a critical detail. The Promise resolves even when the server returns 404 or 500. If you want to know whether the request actually succeeded, you have to check response.ok yourself.

javascript
fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => {
    if (!response.ok) throw new Error(response.status);
    return response.json();
  })
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

The .catch() block only fires for network-level failures: the user is offline, DNS resolution fails, or CORS blocks the request before headers arrive. Server errors like 404 or 500 do not trigger .catch(). You handle them inside .then() by checking response.ok.

How to fetch data and render it

Here is a minimal example that loads ten users from the JSONPlaceholder API and renders them in an HTML list. The pattern works anywhere you need to display fetched data in the DOM.

html
<h1>Authors</h1>
<ul id="authors"></ul>

<script>
  const ul = document.getElementById('authors');
  const list = document.createDocumentFragment();
  const url = 'https://jsonplaceholder.typicode.com/users';

  fetch(url)
    .then((response) => {
      if (!response.ok) throw new Error(response.status);
      return response.json();
    })
    .then((users) => {
      users.forEach((user) => {
        const li = document.createElement('li');
        li.textContent = user.name;
        list.appendChild(li);
      });
      ul.appendChild(list);
    })
    .catch((error) => console.error('Fetch failed:', error));
</script>

Using a DocumentFragment batches DOM updates. You add all list items to the fragment first, then append the fragment to the page once. This avoids multiple reflows and keeps rendering fast when you have dozens or hundreds of items.

How to send POST requests with Fetch

GET is the default. For POST, pass an options object with method, headers, and body. The body must be a string, so call JSON.stringify() on your data object.

javascript
fetch('https://jsonplaceholder.typicode.com/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: 'New Post',
    body: 'This is the content.',
    userId: 1
  })
})
  .then((response) => {
    if (!response.ok) throw new Error(response.status);
    return response.json();
  })
  .then((data) => console.log('Created:', data))
  .catch((error) => console.error('Error:', error));

Set Content-Type to application/json so the server knows how to parse the body. Omitting this header is a common bug. The request goes through, but the server reads an empty or malformed payload.

Why you must handle errors explicitly

Fetch's Promise design catches many developers off guard. A 404 response is not a rejected Promise. It is a resolved Promise with response.ok set to false. If you skip the check, your code proceeds as if the request succeeded, and you get undefined or empty data downstream.

Code sample: fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then((data) => {
// Safe to use data here
})
.catch((error) => {
// Handles network errors AND HTTP errors you threw
console.error(error);
});

Throwing inside .then() forwards the error to .catch(). This unifies your error handling: network failures and HTTP errors both land in the same block.

How to use async/await with Fetch

Promises chain well, but deeply nested .then() calls become hard to read. async/await flattens the structure. You write code that looks synchronous but runs asynchronously.

Code sample: async function getUsers() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) throw new Error(response.status);
const users = await response.json();
console.log(users);
} catch (error) {
console.error('Request failed:', error);
}
}

getUsers();

The try/catch block handles both network errors and HTTP errors you throw. This is the same pattern as the Promise chain, just easier to scan.

How to cancel a Fetch request

Sometimes you need to cancel a request. The user navigates away, or a newer request makes the old one irrelevant. AbortController handles this.

Code sample: const controller = new AbortController();
const signal = controller.signal;

fetch('https://jsonplaceholder.typicode.com/users', { signal })
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
} else {
console.error('Fetch error:', error);
}
});

// Cancel the request after 100ms
setTimeout(() => controller.abort(), 100);

Pass the signal option to fetch(). When you call controller.abort(), the Promise rejects with an AbortError. Check error.name to distinguish cancellations from real failures.

This matters in React, Vue, and other frameworks where components unmount before requests complete. Without cancellation, you get "Can't perform a state update on an unmounted component" warnings. AbortController prevents them.

Fetch vs Ajax vs Axios

Ajax is the general concept of asynchronous requests. XMLHttpRequest was the original implementation. jQuery wrapped it with $.ajax(). Fetch is the modern browser-native replacement. Axios is a third-party library that adds features on top.

FeatureFetchXMLHttpRequestAxios
Promise-basedYesNo (callbacks)Yes
Built into browsersYesYesNo (requires install)
Auto JSON parsingNo (call response.json())NoYes
HTTP error rejectionNo (must check response.ok)NoYes
Request cancellationAbortControllerxhr.abort()CancelToken / AbortController
InterceptorsNoNoYes

Axios rejects the Promise on HTTP errors automatically and parses JSON responses without an extra step. If you are building a large app with auth tokens, retry logic, or request/response interceptors, Axios saves boilerplate. For simpler needs, Fetch works without adding a dependency.

Using Fetch in React, Vue, and Node.js

In React, call fetch() inside useEffect with a cleanup function that aborts the request. In Vue 3, use onMounted and onUnmounted with AbortController. Node.js 18 added a global fetch(), so you can use the same code on client and server.

The API surface is identical across environments. The only difference: browsers enforce CORS, while Node.js does not. If your fetch works in Node but fails in the browser with a CORS error, the issue is server-side headers, not your fetch code.

Also Read
How to configure PHP-FPM with NGINX on Ubuntu

Server configuration for APIs often pairs with frontend HTTP requests

Frequently Asked Questions

Does fetch() reject the Promise on 404 or 500 errors?

No. Fetch only rejects on network failures. HTTP error codes like 404 or 500 resolve the Promise normally. Check response.ok to detect them.

How do I send headers with a Fetch request?

Pass an options object as the second argument: fetch(url, { headers: { 'Authorization': 'Bearer token' } }).

Can I use Fetch in Node.js?

Yes, starting with Node.js 18. Earlier versions require a polyfill like node-fetch.

What is the difference between Fetch and Axios?

Axios auto-parses JSON, rejects on HTTP errors, and supports interceptors. Fetch is built-in and lighter but requires more manual handling.

How do I cancel a Fetch request?

Create an AbortController, pass its signal to fetch(), and call controller.abort() when needed.

ℹ️

Logicity's Take

Fetch's design decision to resolve on HTTP errors is intentional, not a bug. It separates transport-level success (did we reach the server?) from application-level success (did the server do what we asked?). This makes sense for APIs that return structured error bodies you want to parse. But it does mean every Fetch call needs that response.ok check, or a wrapper function that adds it. If you are writing more than a few fetch calls, write a utility that handles this once.

ℹ️

Need Help Implementing This?

Building an API integration or modernizing legacy Ajax code? Logicity connects you with developers who ship production-grade JavaScript. Contact us to scope your project.

M

Manaal Khan

Tech & Innovation Writer

Related Articles