Native JavaScript Fetch API — Handle HTTP Requests Without External Libraries
> cd .. / HUB_EDITORIALE
Sviluppo di siti web

Native JavaScript Fetch API — Handle HTTP Requests Without External Libraries

[2026-07-23] Author: Ing. Calogero Bono
> share
Zenithby Meteora Web The operating system for your business. Social, clients, bookings and invoices in one platform. Gyms, barbers, professionals. Discover Zenith Free demo · no card

Still importing Axios or jQuery just to make an HTTP request? If your project needs two libraries before writing any logic, something is off. The Fetch API is native, modern, and covers 95% of real-world use cases. Here at Meteora Web we use it every day to build applications that talk to REST APIs without bloating the bundle. In this guide we'll dive into how to leverage it fully, with ready-to-copy examples and best practices born from field experience.

How does the Fetch API compare to XMLHttpRequest?

The Fetch API was introduced to replace the old XMLHttpRequest (XHR) with a cleaner, Promise-based interface. While XHR required nested callbacks and manual state handling, Fetch returns a Promise that resolves to a Response object. This allows chaining with .then() or async/await in a linear way.

Basic example:

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

With async/await it becomes even more readable:

async function getData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error:', error);
  }
}

Compared to XHR, Fetch is more concise, natively integrated (zero dependencies), and natively supports CORS, credentials, and streams. The only historical downsides are the lack of native timeout and HTTP error handling (Fetch does not reject the Promise for 4xx or 5xx status). Let's see how to fix both.

Sponsored Protocol

Official reference: MDN Fetch API

How to handle errors and timeouts with the Fetch API?

A common pitfall: if a server responds with 404 or 500, Fetch does not throw an exception. The Promise resolves anyway, and you must manually check the status code. Here's how to do it correctly:

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

Timeout: Fetch has no native timeout. You work around it with AbortController. A pattern we use in our projects:

async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);
  try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(id);
    if (!response.ok) throw new Error('HTTP error');
    return await response.json();
  } catch (error) {
    clearTimeout(id);
    if (error.name === 'AbortError') {
      throw new Error('Request timed out');
    }
    throw error;
  }
}

This ensures requests don't hang for minutes blocking the UI.

Sponsored Protocol

What are the best practices for data fetching in production?

When building a real application, you cannot just fire a fetch every time you need data. Here are some rules we apply daily:

1. Centralize API calls
Don't scatter fetch across components. Create a services/api.js module with reusable functions:

export async function getUsers() {
  const res = await fetch('/api/users');
  if (!res.ok) throw new Error('Error loading users');
  return res.json();
}

export async function createUser(data) {
  const res = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
  if (!res.ok) throw new Error('Error creating user');
  return res.json();
}

2. Use an interceptor for auth and logging
Instead of passing tokens manually, create a custom fetch wrapper:

Sponsored Protocol

const apiFetch = async (url, options = {}) => {
  const token = localStorage.getItem('token');
  const headers = {
    'Content-Type': 'application/json',
    ...options.headers,
    ...(token && { 'Authorization': `Bearer ${token}` })
  };
  try {
    const response = await fetch(url, { ...options, headers });
    if (!response.ok) {
      if (response.status === 401) {
        // redirect to login
        window.location.href = '/login';
      }
      throw new Error(`API error: ${response.status}`);
    }
    return response.json();
  } catch (error) {
    console.error('API call failed:', url, error);
    throw error;
  }
};

3. Manage loading and empty states
Every async request should have three states: loading, success, error. A simple pattern for React:

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    fetch(url)
      .then(res => { if (!res.ok) throw new Error(); return res.json(); })
      .then(data => { if (!cancelled) { setData(data); setLoading(false); } })
      .catch(err => { if (!cancelled) { setError(err); setLoading(false); } });
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

If you use React, this hook prevents memory leaks from unmounted components.

Sponsored Protocol

How to handle concurrent and cancellable requests?

Often you need multiple parallel calls (e.g., dashboard with independent widgets) or to cancel an ongoing request when the user navigates away. Fetch + Promise.all solves the first scenario:

const [users, posts, comments] = await Promise.all([
  fetch('/api/users').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/comments').then(r => r.json())
]);

For cancellation, we use AbortController as above but in a component mount/unmount context. A robust pattern:

function createCancellableFetch() {
  let controller = null;
  
  async function fetchCancellable(url, options) {
    if (controller) controller.abort();
    controller = new AbortController();
    const response = await fetch(url, { ...options, signal: controller.signal });
    return response.json();
  }
  
  function cancelAll() {
    if (controller) controller.abort();
  }
  
  return { fetchCancellable, cancelAll };
}

This is great for autocomplete or real-time search.

Sponsored Protocol

What to do next: summary

  1. Replace Axios with Fetch in new projects. For existing ones, consider refactoring if bundle size is a concern.
  2. Implement error handling and timeout with AbortController. Copy the two snippets above.
  3. Create a services module that centralizes all API calls in your project.
  4. Add an authentication wrapper that automatically injects the token.
  5. Test request cancellation in components that unmount (e.g., during navigation).

The Fetch API is not just a modern alternative: it's the right tool when you want to keep control over dependencies and write code you understand down to the last line. For more on modern JavaScript features, check out our pillar on JavaScript ES2024+.

> share
Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere informatico, fondatore di Meteora Web e Zenith OS. System administrator e progettista di piattaforme, app e CMS proprietari, con esperienza in sviluppo full-stack, marketing digitale ed ecosistema Google.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()