JavaScript Fetch API Complete Guide: GET, POST, Error Handling & AbortController
The Fetch API is the modern standard for making HTTP requests in JavaScript. Introduced in ES2015 and available in all modern browsers, it replaced the older XMLHttpRequest object with a cleaner, Promise-based interface. Whether you are building a single-page application, consuming REST APIs, or submitting form data, Fetch is the tool you need.
This guide covers everything from basic GET requests to advanced patterns like request cancellation with AbortController, timeout handling, and robust error management.
Basic GET Request
The simplest use of Fetch is a GET request. It returns a Promise that resolves to a Response object. You must call .json() to parse the response body as JSON.
// Using .then() chains
fetch('https://api.example.com/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// Using async/await (recommended)
async function getUsers() {
try {
const response = await fetch('https://api.example.com/users');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
The async/await syntax is generally preferred because it makes asynchronous code read like synchronous code, with clear try/catch blocks for error handling.
POST Request with JSON Body
To send data to a server, use the method, headers, and body options. When sending JSON, set the Content-Type header and stringify the body.
async function createUser(userData) {
try {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const newUser = await response.json();
return newUser;
} catch (error) {
console.error('Failed to create user:', error);
}
}
// Usage
createUser({ name: 'Alice', email: 'alice@example.com' });
Understanding Fetch Error Handling
One of the most common mistakes with Fetch is assuming that a .catch() block will handle HTTP errors like 404 or 500. It will not. Fetch only rejects the Promise on network failures — DNS errors, connection refused, CORS issues, or when the request is aborted.
HTTP error responses (4xx and 5xx status codes) are still considered "successful" from the network perspective. The server responded; it just responded with an error. You must check response.ok or response.status manually.
async function fetchData(url) {
try {
const response = await fetch(url);
// Check for HTTP errors
if (!response.ok) {
if (response.status === 404) {
throw new Error('Resource not found');
}
if (response.status === 401) {
throw new Error('Unauthorized — please log in');
}
if (response.status >= 500) {
throw new Error('Server error — try again later');
}
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// This catches network errors AND our thrown HTTP errors
if (error.name === 'TypeError') {
console.error('Network error — check your connection');
} else {
console.error('Request failed:', error.message);
}
throw error; // Re-throw for caller to handle
}
}
Other HTTP Methods
Fetch supports all HTTP methods. Here are the most common patterns:
// PUT — full update
async function updateUser(id, data) {
const response = await fetch(`/api/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return response.json();
}
// PATCH — partial update
async function patchUser(id, data) {
const response = await fetch(`/api/users/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return response.json();
}
// DELETE
async function deleteUser(id) {
const response = await fetch(`/api/users/${id}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Delete failed');
return true;
}
Sending Form Data
When submitting HTML forms, use the FormData object instead of JSON. Fetch automatically sets the correct Content-Type header with the multipart boundary.
async function submitForm(formElement) {
const formData = new FormData(formElement);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData, // No Content-Type header needed!
});
return response.json();
}
// Or build FormData manually
const formData = new FormData();
formData.append('title', 'My File');
formData.append('file', fileInput.files[0]);
Custom Headers and Authentication
Pass custom headers using the Headers object or a plain object. Common use cases include authentication tokens, API keys, and caching control.
const headers = new Headers({
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key',
'Accept': 'application/json',
});
const response = await fetch('/api/protected', { headers });
AbortController: Cancel Requests
The AbortController API lets you abort fetch requests that are no longer needed. This is essential for search-as-you-type inputs, navigation away from a page before a request completes, and cleanup in React useEffect hooks.
// Basic cancellation
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('Request was cancelled');
}
});
// Cancel after 5 seconds
controller.abort();
Fetch Timeout Pattern
Fetch does not have a built-in timeout option. Combine AbortController with setTimeout to implement request timeouts:
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timed out after ${timeout}ms`);
}
throw error;
}
}
// Usage: 3-second timeout
fetchWithTimeout('/api/data', {}, 3000);
Debounced Search with AbortController
A common real-world pattern is cancelling previous requests when the user types in a search box:
let searchController = null;
async function searchProducts(query) {
// Cancel the previous request
if (searchController) {
searchController.abort();
}
searchController = new AbortController();
const response = await fetch(`/api/search?q=${query}`, {
signal: searchController.signal,
});
return response.json();
}
Request and Response Objects
The Fetch API provides Request and Response objects that give you full control over HTTP details:
// Response properties
const response = await fetch(url);
console.log(response.status); // 200, 404, etc.
console.log(response.statusText); // 'OK', 'Not Found', etc.
console.log(response.ok); // true for 200-299
console.log(response.headers.get('Content-Type'));
console.log(response.url); // Final URL (after redirects)
// Other response body methods
const text = await response.text(); // Plain text
const blob = await response.blob(); // Binary data
const formData = await response.formData(); // Form data
const json = await response.json(); // JSON
CORS Mode and Credentials
Control cross-origin behavior with the mode and credentials options:
fetch('https://api.other-domain.com/data', {
mode: 'cors', // 'cors' | 'no-cors' | 'same-origin'
credentials: 'include', // 'include' | 'same-origin' | 'omit'
});
Use credentials: 'include' when you need to send cookies with cross-origin requests (the server must set Access-Control-Allow-Credentials: true).
Fetch Wrapper for Production Use
Here's a reusable fetch wrapper that combines all the best practices discussed in this guide:
class ApiClient {
constructor(baseURL, defaultHeaders = {}) {
this.baseURL = baseURL;
this.defaultHeaders = defaultHeaders;
}
async request(endpoint, options = {}) {
const { timeout = 10000, ...fetchOptions } = options;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const url = `${this.baseURL}${endpoint}`;
try {
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal,
headers: {
...this.defaultHeaders,
...fetchOptions.headers,
},
});
clearTimeout(timeoutId);
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
throw error;
}
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request to ${endpoint} timed out`);
}
throw error;
}
}
get(endpoint, params = {}, options = {}) {
const query = new URLSearchParams(params).toString();
const url = query ? `${endpoint}?${query}` : endpoint;
return this.request(url, { ...options, method: 'GET' });
}
post(endpoint, body, options = {}) {
return this.request(endpoint, {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options.headers },
body: JSON.stringify(body),
});
}
put(endpoint, body, options = {}) {
return this.request(endpoint, {
...options,
method: 'PUT',
headers: { 'Content-Type': 'application/json', ...options.headers },
body: JSON.stringify(body),
});
}
delete(endpoint, options = {}) {
return this.request(endpoint, { ...options, method: 'DELETE' });
}
}
// Usage
const api = new ApiClient('https://api.example.com', {
'Authorization': 'Bearer your-token',
});
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'Bob' });
Frequently Asked Questions
Fetch only rejects on network failures (no connection, DNS error, CORS). HTTP error responses (4xx, 5xx) are still considered successful from a network perspective. You must check response.ok or response.status manually.
Create an AbortController, pass its signal property as the signal option in fetch(), then call controller.abort() to cancel the request.
Fetch has no built-in timeout. Combine AbortController with setTimeout to create a timeout pattern: call controller.abort() after a specified delay.
Use the FormData object as the request body. Fetch automatically sets the correct Content-Type header with the multipart boundary.
Fetch is a built-in browser API (no dependencies), while axios is a third-party library. Axios provides automatic JSON parsing, request/response interceptors, and simpler error handling out of the box. Fetch requires manual handling for HTTP errors and JSON parsing.