Mastering the Fetch API – Modern HTTP Requests in JavaScript

The Fetch API is the modern replacement for XMLHttpRequest and the foundation of how JavaScript applications communicate with servers. It is available in every modern browser and in Node.js since version 18. This guide covers everything from basic usage to patterns you will use in production.

The Basics

A fetch request returns a Promise that resolves to a Response object:

1
2
3
4
fetch('https://api.example.com/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Request failed:', error));

The .json() method reads the response body and parses it as JSON. It returns a Promise, which is why there are two .then() calls. Other body-reading methods include .text(), .blob(), .formData(), and .arrayBuffer().

The async/await version is cleaner and is preferred in modern code:

1
2
3
4
5
6
7
8
9
10
11
12
13
async function getUsers() {
try {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
}
const users = await response.json();
return users;
} catch (error) {
console.error('Failed to fetch users:', error);
throw error;
}
}

The critical detail that trips up beginners is that fetch() only rejects the Promise on network errors — the request could not reach the server at all. HTTP error responses like 404 or 500 do not cause a rejection. The fetch succeeds, and you get a Response object with ok: false. You must check response.ok or response.status yourself. This is by design, not a bug — the server successfully responded, just with an error status.

Sending Data

Beyond GET requests, fetch accepts a second argument: an options object. The most common options are method, headers, and body:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Jain Chen',
email: 'jain@example.com',
}),
});

if (!response.ok) {
const error = await response.json();
console.error('Server error:', error.message);
return;
}

const newUser = await response.json();
console.log('Created:', newUser);

For file uploads, use FormData instead of JSON. Do not set the Content-Type header when using FormData — the browser sets it automatically with the correct multipart boundary:

1
2
3
4
5
6
7
8
9
const formData = new FormData();
formData.append('avatar', fileInput.files[0]);
formData.append('name', 'Jain Chen');

const response = await fetch('/api/users/avatar', {
method: 'POST',
body: formData,
// Do not set Content-Type — browser handles multipart boundary
});

Aborting Requests

In single-page applications, a user might navigate away from a page before a fetch completes. Continuing to process the response wastes resources and can cause state updates on unmounted components. AbortController solves this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const controller = new AbortController();
const signal = controller.signal;

// Auto-abort after 10 seconds
const timeoutId = setTimeout(() => controller.abort(), 10000);

try {
const response = await fetch('/api/large-dataset', { signal });
const data = await response.json();
clearTimeout(timeoutId);
// Process data
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was aborted');
} else {
console.error('Request failed:', error);
}
}

The AbortError is thrown when the request is cancelled. It is important to distinguish it from actual network errors so you do not log it as a real failure.

A common pattern in React: abort a fetch when a component unmounts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
useEffect(() => {
const controller = new AbortController();

async function loadData() {
try {
const response = await fetch(url, { signal: controller.signal });
const data = await response.json();
setData(data);
} catch (error) {
if (error.name !== 'AbortError') {
setError(error.message);
}
}
}

loadData();

return () => controller.abort();
}, [url]);

The cleanup function returned from useEffect calls controller.abort(), which aborts any in-flight request when the component unmounts or the dependency changes. This prevents the classic “setState on unmounted component” warning.

Custom Request Objects

For reusable request configurations, construct a Request object:

1
2
3
4
5
6
7
const request = new Request('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});

const response = await fetch(request);

Request objects are useful for creating a base configuration and overriding specific properties for different calls. They can also be passed between functions as a single unit rather than spreading multiple configuration arguments.

Handling Different Response Types

Not every endpoint returns JSON. Handle each type appropriately:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// JSON (most common)
const user = await response.json();

// Plain text
const markdown = await response.text();

// Binary data (images, PDFs)
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
imgElement.src = imageUrl;

// Streaming large responses
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Process chunk
}

For streaming, the response body is a ReadableStream. This is useful for processing large datasets in chunks rather than loading everything into memory. Server-Sent Events and streaming API responses are increasingly common, and the Fetch API provides the low-level primitives to handle them.

Error Handling Strategy

A production-grade fetch wrapper should handle network errors, HTTP errors, and timeouts consistently:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
async function apiFetch(url, options = {}) {
const controller = new AbortController();
const timeout = options.timeout || 30000;
const timeoutId = setTimeout(() => controller.abort(), timeout);

const defaultHeaders = {
'Content-Type': 'application/json',
};

try {
const response = await fetch(url, {
...options,
headers: { ...defaultHeaders, ...options.headers },
signal: controller.signal,
});

clearTimeout(timeoutId);

if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
const error = new Error(
errorBody.message || `Request failed with status ${response.status}`
);
error.status = response.status;
error.body = errorBody;
throw error;
}

return await response.json();
} catch (error) {
clearTimeout(timeoutId);

if (error.name === 'AbortError') {
const timeoutError = new Error('Request timed out');
timeoutError.status = 408;
throw timeoutError;
}

throw error;
}
}

This wrapper provides automatic timeout handling, consistent error formatting, and JSON parsing. It can be extended with retry logic, request deduplication, and caching. The key principle is that every part of your application that makes HTTP requests should use the same wrapper, so error handling and request configuration are consistent.

Common Patterns

For parallel independent requests, use Promise.all:

1
2
3
4
5
const [users, posts, comments] = await Promise.all([
apiFetch('/api/users'),
apiFetch('/api/posts'),
apiFetch('/api/comments'),
]);

For dependent requests where one call depends on the result of another, do not use Promise.all. Chain them sequentially:

1
2
const user = await apiFetch(`/api/users/${userId}`);
const posts = await apiFetch(`/api/users/${user.id}/posts`);

For uploading with progress tracking, fetch does not directly support upload progress events. Use XMLHttpRequest for that specific use case, or use the fetch ReadableStream for download progress tracking.

The Fetch API is simple at the surface and capable of handling most HTTP communication patterns in modern web applications. The combination of async/await, AbortController, and a consistent wrapper provides everything you need for clean, maintainable API communication.


Mastering the Fetch API – Modern HTTP Requests in JavaScript
https://toongs.org/2026/06/26/19-javascript-fetch-api/
Author
Jain Chen
Posted on
June 26, 2026
Licensed under