JavaScript

JavaScript Fetch API: The Complete Guide (2026)

Master the Fetch API — from basic GET/POST requests to error handling, file uploads, streaming, AbortController, and real-world patterns used in production.

March 20, 20263 min read

Why Fetch Over XMLHttpRequest or Axios?

The Fetch API is built into every modern browser and Node.js 18+. No npm install, no bundle size increase, no dependency to maintain.

Fetch advantages:
  • Built-in — no external dependency
  • Promise-based — works naturally with async/await
  • Streaming support — process large responses in chunks
  • AbortController — cancel requests cleanly
  • Available in browsers, Node.js, Deno, Bun, and service workers

When to use Axios instead:
  • You need automatic request/response interceptors
  • You need built-in request timeout (Fetch requires AbortController)
  • You want automatic JSON parsing (Fetch requires manual .json() call)
  • You need progress events for file uploads

Basic GET Request

// Simple GET

const response = await fetch('https://api.example.com/users');

const data = await response.json();

console.log(data);

Important: Fetch does NOT throw on HTTP errors (404, 500). You must check response.ok:
async function fetchJSON(url) {

const response = await fetch(url);

if (!response.ok) {

throw new Error(HTTP ${response.status}: ${response.statusText});

}

return response.json();

}

try {

const users = await fetchJSON('/api/users');

console.log(users);

} catch (error) {

console.error('Failed to fetch users:', error.message);

}

This is the most common Fetch mistake — forgetting that fetch() only rejects on network errors, not HTTP errors.

POST with JSON Body

const response = await fetch('/api/users', {

method: 'POST',

headers: {

'Content-Type': 'application/json',

'Authorization': Bearer ${token},

},

body: JSON.stringify({

name: 'Alice',

email: 'alice@example.com',

}),

});

if (!response.ok) {

const error = await response.json();

throw new Error(error.detail || 'Request failed');

}

const newUser = await response.json();

console.log('Created:', newUser);

Key points:
  • Always set Content-Type: application/json for JSON bodies
  • Always JSON.stringify() the body — Fetch doesn't do this automatically
  • Parse error responses too — APIs usually return error details in JSON

File Upload with FormData

async function uploadFile(file) {

const formData = new FormData();

formData.append('file', file);

formData.append('quality', 'high');

const response = await fetch('/api/v1/convert/pdf-to-word', {

method: 'POST',

body: formData,

// Do NOT set Content-Type — browser sets it with boundary

});

if (!response.ok) {

throw new Error('Upload failed');

}

// Download the converted file

const blob = await response.blob();

const url = URL.createObjectURL(blob);

const a = document.createElement('a');

a.href = url;

a.download = 'converted.docx';

a.click();

URL.revokeObjectURL(url);

}

// Usage with file input

const input = document.querySelector('input[type="file"]');

input.addEventListener('change', () => {

uploadFile(input.files[0]);

});

Critical: Do NOT set the Content-Type header when using FormData. The browser needs to set it automatically with the correct multipart boundary string.

Cancelling Requests with AbortController

AbortController lets you cancel in-flight requests — essential for search-as-you-type and component unmounting:

let controller = null;

async function search(query) {

// Cancel previous request

if (controller) {

controller.abort();

}

controller = new AbortController();

try {

const response = await fetch(/api/search?q=${query}, {

signal: controller.signal,

});

const results = await response.json();

renderResults(results);

} catch (error) {

if (error.name === 'AbortError') {

// Request was cancelled — this is expected, don't show error

return;

}

console.error('Search failed:', error);

}

}

// React useEffect cleanup pattern

useEffect(() => {

const controller = new AbortController();

fetch('/api/data', { signal: controller.signal })

.then(res => res.json())

.then(setData)

.catch(err => {

if (err.name !== 'AbortError') setError(err);

});

return () => controller.abort(); // Cleanup on unmount

}, []);

Request Timeout

Fetch has no built-in timeout. Use AbortController with setTimeout:

async function fetchWithTimeout(url, options = {}, timeoutMs = 10000) {

const controller = new AbortController();

const timeout = setTimeout(() => controller.abort(), timeoutMs);

try {

const response = await fetch(url, {

...options,

signal: controller.signal,

});

return response;

} catch (error) {

if (error.name === 'AbortError') {

throw new Error(Request timed out after ${timeoutMs}ms);

}

throw error;

} finally {

clearTimeout(timeout);

}

}

// Usage

try {

const response = await fetchWithTimeout('/api/slow-endpoint', {}, 5000);

const data = await response.json();

} catch (error) {

console.error(error.message); // "Request timed out after 5000ms"

}

This is a production-ready pattern. Wrap it in a utility function and use it everywhere instead of raw fetch().

javascriptfetch apihttpasyncweb developmentapitutorial

Related Articles