JavaScript fetch vs Axios: Handling APIs and Errors
Every modern web application talks to an external server sooner or later. Whether you are loading a user profile, submitting an order form, or fetching weather updates, you need a reliable way to make HTTP requests in JavaScript.
For years, developers have debated between two main options: the browser’s built-in Fetch API and the popular third-party library Axios.
Both handle HTTP calls using JavaScript Promises, but they handle data transformation, default configurations, and—most importantly—errors very differently.
If you are a beginner or intermediate developer trying to decide which tool fits your project, or if you have ever been bitten by a fetch() call that silently succeeded on a 404 Not Found response, this guide is for you. We will compare them side by side with real, runnable code.
Part 1: Installation and Basic GET Requests
The most obvious difference starts right at the setup stage.
fetch()is built directly into modern browsers and Node.js (version 18 and newer). You do not need to install anything or add build dependencies.- Axios is an external package. You must install it via npm or include it through a script tag.
To install Axios in a project:
npm install axios
Fetching Data: Side-by-Side
Let’s fetch a list of posts from the free JSONPlaceholder testing API.
Using fetch()
async function getPostsWithFetch() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const data = await response.json();
console.log('Fetch result:', data.title);
}
getPostsWithFetch();
Using Axios
import axios from 'axios';
async function getPostsWithAxios() {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1');
console.log('Axios result:', response.data.title);
}
getPostsWithAxios();
Key Differences in GET Requests
- Two-step resolution in
fetch: When usingfetch(), the first Promise resolves to aResponsestream object. To access the JSON body, you must explicitly call and awaitresponse.json(). - Automatic transformation in Axios: Axios automatically checks the response header and parses JSON into a plain JavaScript object, placing it directly under
response.data.
Part 2: Sending Data with POST Requests
When sending payloads to an API, you usually convert your data into a JSON string and specify the Content-Type: application/json header.
Let’s look at how both tools handle creating a new resource.
The Side-by-Side POST Comparison
Using fetch()
async function createPostWithFetch() {
const newPost = {
title: 'Learning Fetch',
body: 'Native APIs are powerful!',
userId: 1,
};
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newPost),
});
const data = await response.json();
console.log('Created via Fetch:', data);
}
createPostWithFetch();
Using Axios
import axios from 'axios';
async function createPostWithAxios() {
const newPost = {
title: 'Learning Axios',
body: 'Axios keeps syntax concise!',
userId: 1,
};
const response = await axios.post(
'https://jsonplaceholder.typicode.com/posts',
newPost
);
console.log('Created via Axios:', response.data);
}
createPostWithAxios();
With fetch(), you have to explicitly call JSON.stringify(newPost) and set the 'Content-Type': 'application/json' header manually. If you forget either step, your backend server might receive an empty body or reject the payload with an HTTP 400 Bad Request.
Axios detects that you passed a standard JavaScript object as the payload, sets the Content-Type header to application/json automatically, and serializes the object for you behind the scenes.
Part 3: The Big Difference: Error Handling
This is the single most common source of bugs for developers transitioning between fetch and Axios.
How fetch() Treats Errors
The fetch() Promise does not reject on HTTP error status codes like 404 Not Found or 500 Internal Server Error.
A fetch() promise only rejects if there is a fundamental network breakdown—such as being offline, hitting an invalid domain name, or encountering a blocked CORS preflight request.
If the server responds with a 404, fetch() still treats the request as successful. You must manually verify the response.ok boolean property (which is true only for status codes 200-299):
async function fetchWithErrorHandling() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/invalid-route');
if (!response.ok) {
// Must manually throw to trigger the catch block
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Fetch caught an error:', error.message);
}
}
fetchWithErrorHandling();
How Axios Treats Errors
Axios follows standard Promise conventions: any HTTP status code outside the 2xx range automatically rejects the Promise.
That means a 404 or 500 response skips the rest of the try block and lands immediately in your catch block. Furthermore, Axios attaches useful debugging information—including the status code, request configuration, and any payload the server returned—directly onto the error object:
import axios from 'axios';
async function axiosWithErrorHandling() {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/invalid-route');
console.log(response.data);
} catch (error) {
if (error.response) {
// The server responded with a status code outside the 2xx range
console.error(`Server error: ${error.response.status}`);
console.error('Server error data:', error.response.data);
} else if (error.request) {
// The request was sent but no response was received (e.g., network drop)
console.error('Network error: No response received');
} else {
// Something happened while setting up the request
console.error('Request setup error:', error.message);
}
}
}
axiosWithErrorHandling();
Quick Comparison Matrix
| Feature | fetch() |
Axios |
|---|---|---|
| Installation | Built-in (no install needed) | npm install axios |
| JSON Data Parsing | Manual (await res.json()) |
Automatic (res.data) |
| JSON Serialization | Manual (JSON.stringify()) |
Automatic |
| Rejects on 4xx/5xx? | No (Check res.ok manually) |
Yes (Automatically throws) |
| Request Timeout | Via AbortSignal.timeout(ms) |
Built-in (timeout: 5000) |
| Interceptors | No native support | Native (request & response) |
Part 4: Handling Timeouts
If an API hangs or takes thirty seconds to respond, you should fail gracefully rather than keep your user waiting forever.
Native fetch() with AbortSignal
In modern JavaScript, you can cancel a hung fetch() call using AbortSignal.timeout():
async function fetchWithTimeout() {
try {
// Abort if request takes longer than 3000ms (3 seconds)
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1', {
signal: AbortSignal.timeout(3000),
});
const data = await response.json();
console.log(data);
} catch (error) {
if (error.name === 'TimeoutError') {
console.error('Fetch request timed out!');
} else {
console.error('Fetch error:', error.message);
}
}
}
Axios Timeout Configuration
Axios lets you pass a timeout property in milliseconds directly inside the request options:
import axios from 'axios';
async function axiosWithTimeout() {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1', {
timeout: 3000, // 3 seconds
});
console.log(response.data);
} catch (error) {
if (error.code === 'ECONNABORTED') {
console.error('Axios request timed out!');
} else {
console.error('Axios error:', error.message);
}
}
}
Hands-on Example
Here is a complete, runnable Node.js script comparing both methods against real endpoints.
To run this file, initialize a temporary Node directory:
mkdir api-demo && cd api-demo
npm init -y
npm pkg set type="module"
npm install axios
Save the following code as test-requests.js:
import axios from 'axios';
const VALID_URL = 'https://jsonplaceholder.typicode.com/todos/1';
const BROKEN_URL = 'https://jsonplaceholder.typicode.com/todos/999999';
async function testFetch() {
console.log('--- Testing Native Fetch ---');
// 1. Successful request
try {
const res = await fetch(VALID_URL);
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
const data = await res.json();
console.log('Fetch Success:', data.title);
} catch (err) {
console.error('Fetch Failed:', err.message);
}
// 2. 404 Request
try {
const res = await fetch(BROKEN_URL);
if (!res.ok) {
throw new Error(`Fetch caught HTTP status ${res.status}`);
}
const data = await res.json();
console.log(data);
} catch (err) {
console.log('Fetch caught expected error:', err.message);
}
}
async function testAxios() {
console.log('\n--- Testing Axios ---');
// 1. Successful request
try {
const res = await axios.get(VALID_URL);
console.log('Axios Success:', res.data.title);
} catch (err) {
console.error('Axios Failed:', err.message);
}
// 2. 404 Request
try {
await axios.get(BROKEN_URL);
} catch (err) {
console.log('Axios caught expected error: Status', err.response?.status);
}
}
async function run() {
await testFetch();
await testAxios();
}
run();
Execute it in your terminal:
node test-requests.js
Common Mistakes / Troubleshooting
- Assuming
fetch()rejects on 404 or 500 errors: This is the most common bug in junior codebases. Always checkif (!response.ok)right after awaitingfetch(), before you attempt to callresponse.json(). - Forgetting
await response.json()in fetch: Callingconst data = response.json()withoutawaitreturns an unresolvedPromise<pending>instead of your actual data. - Reading
error.messagealone in Axios: When an Axios request fails due to an HTTP error, the backend’s error message (like"Email already in use") is insideerror.response.data, not inerror.message. - Forgetting JSON stringification in fetch POST calls: If you pass a plain object to
body: myObjectinfetch(), your browser will serialize it as"[object Object]". Always passJSON.stringify(myObject).
Try It Yourself
Open your editor and modify test-requests.js:
- Write a function named
fetchUserPosts(userId)using eitherfetch()orAxios. - Have it query
https://jsonplaceholder.typicode.com/posts?userId=${userId}. - If an invalid ID (like
userId = 0) returns an empty array[], throw a custom error stating:"No posts found for this user." - Log the count of returned posts if successful.
What’s Next
- Axios Interceptors: Discover how to automatically attach JWT authorization tokens to every outgoing request.
- AbortController Patterns: Learn how to cancel active search requests when a user types a new character in an autocomplete bar.
- Creating a Base API Client: See how to create a pre-configured Axios instance with custom base URLs and retry logic.
Bookmark this guide to quickly look up error-handling patterns whenever you start a new JavaScript project.