Axios simplifies making API requests in React, offering better syntax, built-in error handling, and interceptors compared to the native Fetch API. This makes it a popular choice for developers looking to fetch data with Axios in React. In this React Axios tutorial, you will see practical examples of how to fetch data with Axios in React applications.
Axios is promise-based, which gives you the ability to take advantage of JavaScript’s async
and await
for more readable asynchronous code.
You can also intercept and cancel requests, and there’s built-in client-side protection against cross-site request forgery.
In this article, you will see examples of how to use Axios to access the popular JSONPlaceholder API within a React application.
Choosing between Axios and the native Fetch API is a common decision point for React developers. This section provides a side-by-side comparison to highlight why many teams prefer Axios in production environments, while also outlining cases where Fetch may be sufficient for smaller projects.
Feature | Axios | Fetch API |
---|---|---|
Syntax | Axios offers a clean, promise-based syntax that significantly reduces boilerplate code. Developers can quickly chain requests, handle responses, and set global configurations. This streamlined approach makes it easier for teams to maintain consistent API handling across medium and large-scale React applications. | Fetch provides a low-level interface with more verbose syntax. Developers must manually configure headers, parse JSON, and check status codes for every request. While it’s built into the browser and requires no installation, it often leads to repetitive code in larger React projects. |
Error Handling | Axios automatically throws errors for HTTP response codes outside the 2xx range. It provides detailed error objects with response , request , and message properties, making debugging easier. Built-in error handling ensures developers can quickly identify and fix issues without writing repetitive conditional checks. |
Fetch only rejects a promise for network-level failures. HTTP errors like 404 or 500 are treated as resolved promises, requiring manual status checks. Developers must explicitly throw errors based on response.ok , leading to more boilerplate and increased chances of inconsistent error handling in applications. |
Interceptors | Axios includes request and response interceptors, enabling developers to inject authentication tokens, log activity, or transform data globally before reaching components. This feature is particularly valuable in enterprise applications where centralized request management improves security, scalability, and developer productivity across teams. | Fetch does not provide built-in interceptor functionality. To achieve similar outcomes, developers must wrap Fetch in custom utility functions or middleware. This increases complexity, reduces maintainability, and makes centralized request transformations or authentication token injection more challenging in large-scale React applications. |
JSON Handling | Axios automatically transforms JSON responses, eliminating the need for manual .json() parsing. This simplifies data handling and minimizes developer error, allowing teams to focus on application logic rather than repetitive parsing. Automatic JSON handling makes Axios especially attractive in data-heavy React applications. |
Fetch requires developers to manually parse JSON using .json() . This additional step increases boilerplate and introduces the risk of errors if developers forget to parse or incorrectly chain promises. While straightforward for small apps, it becomes repetitive and error-prone in larger React projects. |
Request Cancellation | Axios supports request cancellation natively through the CancelToken API and the newer AbortController integration. This is essential for React apps with dynamic UI states where requests may need to be aborted, such as live search or component unmounts, improving performance and user experience. |
Fetch supports cancellation through the AbortController API. However, it requires manual setup and integration, making it less intuitive than Axios. Developers often need to write additional code to manage cancellations effectively, which can be cumbersome in complex React applications handling multiple parallel requests. |
Axios offers a superior developer experience compared to Fetch: Axios provides a cleaner, promise-based syntax that reduces boilerplate, automatically parses JSON responses, and includes built-in error handling for HTTP status codes. This makes it ideal for medium and large React applications. In contrast, the native Fetch API is lightweight and built-in, but requires manual status checks, JSON parsing, and lacks features like interceptors, making it better suited for small utilities or simple use cases.
Comprehensive request patterns with real-world examples: This guide walks through making GET, POST, and DELETE requests using both React class components and hooks. Each example demonstrates how to handle loading and error states, update UI responsively, and cancel in-flight requests to prevent memory leaks or unwanted state updates after unmounting.
Centralized configuration and interceptors for authentication and telemetry: By creating a shared Axios instance and registering request and response interceptors, you can automatically attach authentication tokens (such as Authorization: Bearer <token>
), add correlation IDs for tracing, and normalize error handling across your app. This ensures consistent security, observability, and error reporting, and allows for advanced flows like automatic token refresh and retry on 401 errors.
Async/await for readable and maintainable code: Leveraging async/await
with Axios keeps asynchronous logic top-to-bottom and makes error handling explicit with try/catch
blocks. This approach integrates naturally with React hooks like useEffect
and useState
, resulting in more readable, maintainable, and bug-resistant data fetching code.
Robust error handling for resilient user experiences: The article details how to branch on err.response
(HTTP errors), err.request
(network errors), and err.message
(setup/timeouts) to provide user-friendly UI messages, log diagnostic details for developers, and implement retries or alternative flows. Centralizing error handling ensures that users see clear, actionable feedback and that sensitive information is never exposed in the UI.
Advanced Axios patterns for real-world requirements: Beyond basic CRUD, Axios supports advanced use cases such as paginated data fetching with query parameters, concurrent requests using Promise.all
, file uploads with progress tracking via onUploadProgress
, and custom timeout handling. These features enable you to build enterprise-grade React applications that are scalable, performant, and user-friendly.
Best practices for maintainable React apps: The guide emphasizes separating API logic from UI components, creating reusable hooks (like useAxios
), and always canceling requests on unmount. It also covers when to choose Axios over Fetch, how to type responses and errors in TypeScript, and how to avoid common security pitfalls such as exposing tokens or stack traces in the UI.
Need to deploy a React project and have it live? Check out DigitalOcean App Platform and deploy a React project directly from GitHub in minutes.
To follow along with this article, you’ll need the following:
npm install axios
) — this article demonstrates how to install and use it in React projects. The examples here are tested with Axios version 1.x. See the Axios GitHub repository for documentation and updates.This tutorial was verified with Node.js v20.11.1, npm v10.2.4, react
v18.2.0, and axios
v1.6.x.
Now, you will learn how to install Axios in React and add it to a project you created following the How to Set up a React Project with Create React App tutorial.
- npx create-react-app react-axios-example
To add Axios to the project, open your terminal and change directories into your project:
- cd react-axios-example
Then run this command to install Axios:
- npm install axios
- yarn add axios
Both npm and Yarn will install the latest stable version of Axios, ensuring your project uses the most up-to-date features and security fixes.
npm install axios
(or yarn add axios
).npm list axios
→ shows a version like axios@1.x
.cat package.json
→ dependencies
contains "axios": "^1.x"
.import axios from 'axios'
to a file; the dev server should compile without errors.$ npm list axios
project@1.0.0 /path/to/project
└── axios@1.6.x
package.json
import axios
without a build errorIn this example, you create a new component and import Axios into it to send a GET
request.
Inside your React project, you will need to create a new component named PersonList
.
First, create a new components
subdirectory in the src
directory:
- mkdir src/components
In this directory, create PersonList.js
and add the following code to the component:
import React from 'react';
import axios from 'axios';
export default class PersonList extends React.Component {
state = {
persons: []
}
componentDidMount() {
axios.get(`https://jsonplaceholder.typicode.com/users`)
.then(res => {
const persons = res.data;
this.setState({ persons });
})
.catch(err => {
console.error('Error fetching data:', err);
});
}
render() {
return (
<ul>
{
this.state.persons
.map(person =>
<li key={person.id}>{person.name}</li>
)
}
</ul>
)
}
}
First, you import React and Axios so that both can be used in the component. Then you hook into the componentDidMount
lifecycle hook and perform a GET
request.
You use axios.get(url)
with a URL from an API endpoint to get a promise which returns a response object. Inside the response object, there is data that is then assigned the value of person
.
Axios supports both the .then()
promise style and the modern async/await
syntax. While .then()
is straightforward and works well in lifecycle methods, using async/await
can make the code more readable in functional components with hooks.
You can also get other information about the request, such as the status code under res.status
or more information inside of res.request
.
Modern React apps favor functional components and hooks. The example below shows a production‑ready pattern with loading and error states, request cancellation via AbortController
(supported by Axios), and clear separation of concerns. This approach improves UX, avoids setting state on unmounted components, and aligns with enterprise best practices.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function PersonListHooks() {
const [persons, setPersons] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController(); // Axios supports AbortController
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const res = await axios.get('https://jsonplaceholder.typicode.com/users', {
signal: controller.signal,
// headers: { Authorization: `Bearer ${token}` }, // example for auth
});
setPersons(res.data);
} catch (err) {
// Distinguish between cancellation, network errors, and HTTP errors
if (axios.isCancel?.(err) || err.name === 'CanceledError') return;
if (err.response) {
// Server responded with a non-2xx status
setError(`Server error: ${err.response.status} ${err.response.statusText}`);
} else if (err.request) {
// No response received
setError('Network error: no response from server');
} else {
// Something else happened while setting up the request
setError(`Request error: ${err.message}`);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort(); // Cleanup: cancel in-flight request on unmount
}, []); // Empty dependency array: run once on mount
if (loading) return <p>Loading users…</p>;
if (error) return <p role="alert">{error}</p>;
return (
<ul>
{persons.map((person) => (
<li key={person.id}>{person.name}</li>
))}
</ul>
);
}
export default PersonListHooks;
Why this pattern works (EEAT & best practices):
loading
and error
provide accessible UX and clearer logic for retries and skeleton UIs.AbortController
prevents race conditions and memory leaks when a component unmounts or dependencies change.err.response
, err.request
, and other errors leads to actionable logs and safer user messaging.Hooks vs Class Components — when to choose what
this
/lifecycle pitfalls.Add this component to your app.js
:
import PersonList from './components/PersonList.js';
function App() {
return (
<div className="App">
<PersonList/>
</div>
)
}
Alternative (Hooks): Use the hooks version of the list component.
import PersonListHooks from './components/PersonListHooks';
function App() {
return (
<div className="App">
<PersonListHooks />
</div>
);
}
Then run your application:
- npm start
View the application in the browser. You will be presented with a list of 10 names.
src/components/PersonListHooks.js
, import into App
.npm start
.<ul>
<li>Leanne Graham</li>
<li>Ervin Howell</li>
<li>Clementine Bauch</li>
<!-- … 7 more … -->
</ul>
App
and renderedIn this step, you will use Axios with another HTTP request method called POST
.
Below is an updated example of the PersonAdd
component, now using async/await
, logging the HTTP status, and robust error handling:
import React from 'react';
import axios from 'axios';
export default class PersonAdd extends React.Component {
state = {
name: ''
}
handleChange = event => {
this.setState({ name: event.target.value });
}
handleSubmit = async event => {
event.preventDefault();
const user = { name: this.state.name };
try {
const res = await axios.post('https://jsonplaceholder.typicode.com/users', user);
console.log('Status:', res.status);
console.log('Response data:', res.data);
} catch (err) {
if (err.response) {
console.error('POST failed with status:', err.response.status, err.response.statusText);
} else if (err.request) {
console.error('Network error: no response from server');
} else {
console.error('Request setup error:', err.message);
}
}
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<label>
Person Name:
<input type="text" name="name" onChange={this.handleChange} />
</label>
<button type="submit">Add</button>
</form>
</div>
)
}
}
This example uses async/await
for clarity, logs res.status
for visibility into the HTTP result, and includes try/catch
branches that distinguish between server errors (err.response
), network timeouts (err.request
), and request setup issues (err.message
).
Inside the handleSubmit
function, you prevent the default action of the form. Then update the state
to the user
input.
Using POST
gives you the same response object with information that you can use inside of a then
call.
To complete the POST
request, you first capture the user
input. Then you add the input along with the POST
request, which will give you a response. You can then console.log
the response, which should show the user
input in the form.
Add this component to your app.js
:
import PersonList from './components/PersonList';
import PersonAdd from './components/PersonAdd';
function App() {
return (
<div className="App">
<PersonAdd/>
<PersonList/>
</div>
)
}
Alternative (Hooks): Swap in the hooks‑based POST component.
import PersonAddHooks from './components/PersonAddHooks';
function App() {
return (
<div className="App">
<PersonAddHooks />
</div>
);
}
Then run your application:
- npm start
View the application in the browser. You will be presented with a form for submitting new users. Check the console after submitting a new user.
src/components/PersonAddHooks.js
, import into App
.201
or 200
status in console (JSONPlaceholder mocks creation).Status: 201
Response data: { id: 101, name: "Ada Lovelace" }
axios.post
The hooks version below mirrors the class example but adds production-friendly details: loading state, error messages, HTTP status logging, and request cancellation with AbortController
. This keeps the UI responsive and prevents state updates after unmount.
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
function PersonAddHooks() {
const [name, setName] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const lastStatus = useRef(null);
useEffect(() => {
// No initial POST here; controller is created per submission
return () => {
// Cleanup if needed
};
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
const controller = new AbortController();
try {
const res = await axios.post(
'https://jsonplaceholder.typicode.com/users',
{ name },
{ signal: controller.signal }
);
lastStatus.current = res.status;
console.log('Status:', res.status);
console.log('Response data:', res.data);
setName(''); // reset on success
} catch (err) {
if (axios.isCancel?.(err) || err.name === 'CanceledError') return;
if (err.response) {
setError(`POST failed: ${err.response.status} ${err.response.statusText}`);
} else if (err.request) {
setError('Network error: no response from server');
} else {
setError(`Request setup error: ${err.message}`);
}
} finally {
setLoading(false);
}
// Optional: return a function to cancel if you convert this to a long-running op
// return () => controller.abort();
};
return (
<form onSubmit={handleSubmit}>
<label>
Person Name:
<input
type="text"
name="name"
value={name}
onChange={(e) => setName(e.target.value)}
disabled={loading}
/>
</label>
<button type="submit" disabled={loading || !name.trim()}>
{loading ? 'Adding…' : 'Add'}
</button>
{error && <p role="alert">{error}</p>}
{lastStatus.current && <p>Last status: {lastStatus.current}</p>}
</form>
);
}
export default PersonAddHooks;
Tip: Disable the submit button while loading
to prevent duplicate requests. For authenticated APIs, move POST calls to a shared Axios instance with request interceptors for tokens.
In this example, you will see how to delete items from an API using axios.delete
and passing a URL as a parameter.
Inside your React project, you will need to create a new component named PersonRemove
.
Replace the PersonRemove.js
file with the following version, which uses async/await
and robust error handling:
import React from 'react';
import axios from 'axios';
export default class PersonRemove extends React.Component {
state = {
id: ''
}
handleChange = event => {
this.setState({ id: event.target.value });
}
handleSubmit = async event => {
event.preventDefault();
const { id } = this.state;
if (!id) return;
try {
// Note: some production APIs require auth headers; see note below
const res = await axios.delete(`https://jsonplaceholder.typicode.com/users/${id}` /*, {
headers: { Authorization: `Bearer <token>` }
}*/);
console.log('Status:', res.status);
console.log('Response data:', res.data);
} catch (err) {
if (err.response) {
console.error('DELETE failed with status:', err.response.status, err.response.statusText);
} else if (err.request) {
console.error('Network error: no response from server');
} else {
console.error('Request setup error:', err.message);
}
}
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<label>
Person ID:
<input type="number" name="id" onChange={this.handleChange} />
</label>
<button type="submit">Delete</button>
</form>
</div>
)
}
}
Note: API responses vary. In production systems, DELETE
endpoints may require headers (for example, Authorization
bearer tokens or CSRF tokens) or additional parameters. For larger apps, prefer a shared Axios instance with request/response interceptors (see the Interceptors section) to inject auth and telemetry consistently.
The hooks version mirrors the class example and adds loading/error states, status logging, and request cancellation. This prevents race conditions and keeps UI feedback responsive.
import React, { useState, useRef } from 'react';
import axios from 'axios';
function PersonRemoveHooks() {
const [id, setId] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const lastStatus = useRef(null);
const handleSubmit = async (e) => {
e.preventDefault();
if (!id.trim()) return;
setLoading(true);
setError(null);
const controller = new AbortController();
try {
// Some APIs require headers (auth/CSRF). Add via Axios instance or here.
const res = await axios.delete(
`https://jsonplaceholder.typicode.com/users/${id}`,
{ signal: controller.signal }
);
lastStatus.current = res.status;
console.log('Status:', res.status);
console.log('Response data:', res.data);
setId('');
} catch (err) {
if (axios.isCancel?.(err) || err.name === 'CanceledError') return;
if (err.response) {
setError(`DELETE failed: ${err.response.status} ${err.response.statusText}`);
} else if (err.request) {
setError('Network error: no response from server');
} else {
setError(`Request setup error: ${err.message}`);
}
} finally {
setLoading(false);
}
// Optional: return () => controller.abort(); // if you adapt to a long-running flow
};
return (
<form onSubmit={handleSubmit}>
<label>
Person ID:
<input
type="number"
name="id"
value={id}
onChange={(e) => setId(e.target.value)}
disabled={loading}
/>
</label>
<button type="submit" disabled={loading || !id.trim()}>
{loading ? 'Deleting…' : 'Delete'}
</button>
{error && <p role="alert">{error}</p>}
{lastStatus.current && <p>Last status: {lastStatus.current}</p>}
</form>
);
}
export default PersonRemoveHooks;
Tip: For authenticated APIs, prefer a shared Axios instance with request interceptors to inject Authorization
headers and response interceptors to handle 401 refresh flows.
import PersonList from './components/PersonList';
import PersonAdd from './components/PersonAdd';
import PersonRemove from './components/PersonRemove';
function App() {
return (
<div className="App">
<PersonAdd/>
<PersonList/>
<PersonRemove/>
</div>
)
}
Alternative (Hooks): Use the hooks‑based delete component.
import PersonRemoveHooks from './components/PersonRemoveHooks';
function App() {
return (
<div className="App">
<PersonRemoveHooks />
</div>
);
}
Then run your application:
- npm start
View the application in the browser. You will be presented with a form for removing users.
src/components/PersonRemoveHooks.js
, import into App
.1
→ Delete.Status: 200
and {}
in console (JSONPlaceholder returns an empty object).Status: 200
Response data: {}
axios.delete
called with correct pathA shared Axios instance centralizes configuration (base URL, headers, timeouts) and enables interceptors for auth and telemetry. This improves consistency and reduces boilerplate.
// src/api.js
import axios from 'axios';
const API = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com/',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
withCredentials: false, // set true only if your API uses cookies
});
export default API;
// src/api.interceptors.js
import API from './api';
API.interceptors.request.use(
(config) => {
// Example: attach auth token from storage
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Example: add a correlation ID for tracing
config.headers['X-Request-ID'] = crypto.randomUUID?.() || Date.now().toString(36);
return config;
},
(error) => Promise.reject(error)
);
// src/api.interceptors.js (continued)
API.interceptors.response.use(
(response) => response,
async (error) => {
const { response, config } = error;
// Basic telemetry
console.warn('API error:', {
url: config?.url,
method: config?.method,
status: response?.status,
});
// Example: handle expired access token
if (response?.status === 401 && !config.__isRetry) {
config.__isRetry = true;
try {
// Pseudo refresh flow; replace with your auth endpoint
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
// await axios.post('/auth/refresh', { refreshToken });
// localStorage.setItem('access_token', newAccessToken);
// config.headers.Authorization = `Bearer ${newAccessToken}`;
return API(config); // retry original request
}
} catch (e) {
// fall through to reject
}
}
return Promise.reject(error);
}
);
// src/components/PersonRemove.js
import React from 'react';
import API from '../api';
import '../api.interceptors'; // ensure interceptors are registered once
export default class PersonRemove extends React.Component {
state = { id: '' };
handleChange = (e) => this.setState({ id: e.target.value });
handleSubmit = async (e) => {
e.preventDefault();
try {
const res = await API.delete(`users/${this.state.id}`);
console.log(res.data);
} catch (err) {
console.error('Delete failed:', err);
}
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Person ID:
<input type="number" name="id" onChange={this.handleChange} />
</label>
<button type="submit">Delete</button>
</form>
);
}
}
Security note: Never hard-code secrets in frontend code. Store tokens securely and prefer a backend proxy for sensitive operations.
Using async/await
with Axios makes code cleaner and easier to reason about, especially in modern React apps that favor hooks. Instead of chaining .then()
, you can wrap calls in a try/catch
for clearer error handling.
import React from 'react';
import API from '../api';
export default class PersonRemove extends React.Component {
state = { id: '' };
handleChange = (e) => this.setState({ id: e.target.value });
handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await API.delete(`users/${this.state.id}`);
console.log('Status:', response.status);
console.log('Response data:', response.data);
} catch (err) {
if (err.response) {
console.error('Delete failed with status:', err.response.status, err.response.statusText);
} else if (err.request) {
console.error('Network error: no response from server');
} else {
console.error('Request setup error:', err.message);
}
}
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Person ID:
<input type="number" name="id" onChange={this.handleChange} />
</label>
<button type="submit">Delete</button>
</form>
);
}
}
Why async/await in React?
Using async/await
improves readability by keeping logic top‑to‑bottom rather than deeply nested in .then()
chains. This makes error handling with try/catch
explicit and pairs naturally with hooks (useEffect
, useState
) in modern functional components.
Effective error handling is the difference between a resilient React app and a brittle one. This section explains how to handle errors with Axios in React, what the Axios error object contains, and how to produce user‑friendly messages while keeping detailed logs for developers. (Keywords: Axios error handling React, Axios interceptors React, network error, HTTP error).
When working with Axios in React, understanding the structure of the error object is crucial for building robust, user-friendly applications. Axios enhances the standard JavaScript Error
object with additional properties that provide detailed context about what went wrong during an HTTP request. This enables you to distinguish between different failure scenarios—such as server errors, network issues, and request misconfigurations—and respond appropriately in your UI and logs.
Field | When It Exists | What It Contains | How to Use It in React Apps |
---|---|---|---|
err.response |
The server responded, but with a non-2xx status | An object: { status, statusText, data, headers, config, request } |
Display status-aware messages (e.g., “Unauthorized” for 401, “Not Found” for 404, “Server Error” for 5xx). |
err.request |
Request was sent but no response received | The underlying request object (e.g., XMLHttpRequest in browsers, http.ClientRequest in Node.js) |
Treat as a network error; prompt the user to check their connection or retry. |
err.message |
Always present | A human-readable string describing the error (e.g., timeouts, cancellations, misconfigurations) | Show a generic error message, log details for debugging, and confirm if the error was due to cancellation. |
err.code |
Sometimes (e.g., timeouts, network errors) | A short error code string (e.g., 'ECONNABORTED' for timeouts) |
Use for advanced error handling, such as retrying on timeouts or showing specific UI for certain error codes. |
err.isAxiosError |
Always present (Axios >= 0.19.0) | Boolean flag (true if error originated from Axios) |
Safely distinguish Axios errors from other thrown errors in your app or error boundaries. |
err.config |
Always present | The Axios config object used for the request | Useful for debugging or for retrying the request with modified parameters. |
Authentication/Authorization Errors (401/403):
Use err.response.status
to detect when a user is not authenticated or lacks permission. Prompt for login or show an access denied message.
Resource Not Found (404):
If err.response.status === 404
, inform the user that the requested resource does not exist, rather than showing a generic error.
Server Errors (5xx):
For err.response.status >= 500
, consider showing a “Server is temporarily unavailable” message and optionally implement retry logic.
Network Failures:
If err.request
exists but err.response
does not, the request was made but no response was received. This often indicates a network issue or that the server is down. Suggest the user check their connection or try again later.
Timeouts and Cancellations:
If err.code === 'ECONNABORTED'
or err.message
includes “timeout”, inform the user that the request took too long. If the error was due to cancellation (e.g., component unmount), you may want to silently ignore it.
Use these small, reliable checks to classify common error cases and avoid noisy logs:
import axios /*, { AxiosError }*/ from 'axios';
try {
// ...
} catch (err) {
// 1) Narrow to Axios errors (guards against unrelated exceptions)
if (axios.isAxiosError?.(err)) {
// 2) Cancellation (component unmounted / user navigated)
if (err.name === 'CanceledError') {
// Silently ignore or debug-log only
return;
}
// 3) Timeout (Axios sets code ECONNABORTED on timeout)
if (err.code === 'ECONNABORTED' || err.message?.toLowerCase().includes('timeout')) {
// Optionally surface "Request timed out" and suggest retry
}
// 4) Network vs HTTP status
if (err.response) {
// HTTP error: use err.response.status / statusText
} else if (err.request) {
// Network error: no response from server
}
} else {
// Non-Axios error (runtime/logic) — rethrow or handle separately
throw err;
}
}
Environment gotchas (browsers):
err.request
without err.response
). Confirm server CORS headers and preflight handling.import axios from 'axios';
try {
const res = await axios.get('/users');
// use res.data
} catch (err) {
if (err.response) {
console.error('Server error:', err.response.status);
} else if (err.request) {
console.error('Network error:', err.message);
} else {
console.error('Request setup error:', err.message);
}
}
import React, { useEffect, useState } from 'react';
import axios from 'axios';
export function useUsers() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const run = async () => {
setLoading(true);
setError(null);
try {
const res = await axios.get('/users', { signal: controller.signal });
setData(res.data);
} catch (err) {
if (axios.isCancel?.(err) || err.name === 'CanceledError') return;
if (err.response) {
const { status, statusText } = err.response;
setError(`Request failed (${status} ${statusText}). Please try again.`);
} else if (err.request) {
setError('Network error: no response from server.');
} else {
setError(`Request error: ${err.message}`);
}
} finally {
setLoading(false);
}
};
run();
return () => controller.abort();
}, []);
return { data, loading, error };
}
Use a shared instance to normalize errors and attach context (auth, correlation IDs). This keeps component code clean and messages consistent.
// api.js
import axios from 'axios';
const API = axios.create({ baseURL: '/api', timeout: 10000 });
export default API;
// api.errors.js — normalize messages for UI and keep diagnostics for logs
export function normalizeAxiosError(err) {
if (err.response) {
const { status, statusText, data } = err.response;
const ui = status === 401
? 'Please log in to continue.'
: status === 403
? 'You do not have permission to perform this action.'
: status === 404
? 'The requested resource was not found.'
: status >= 500
? 'The server is unavailable right now. Please try again.'
: `Request failed (${status} ${statusText}).`;
return { ui, diag: { status, statusText, data } };
}
if (err.request) return { ui: 'Network error: no response from server.', diag: { message: err.message } };
return { ui: `Request error: ${err.message}`, diag: { message: err.message } };
}
// api.interceptors.js — add context; optionally map errors here
import API from './api';
import { normalizeAxiosError } from './api.errors';
API.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token');
if (token) config.headers.Authorization = `Bearer ${token}`;
config.headers['X-Request-ID'] = crypto.randomUUID?.() || Date.now().toString(36);
return config;
});
API.interceptors.response.use(
(res) => res,
(error) => {
// Attach normalized messages for consumers
error.normalized = normalizeAxiosError(error);
// Basic telemetry
console.warn('API error', {
url: error.config?.url,
method: error.config?.method,
...error.normalized.diag,
});
return Promise.reject(error);
}
);
// Example consumer (component/service)
import API from './api';
import './api.interceptors';
async function deleteUser(id, setError) {
try {
await API.delete(`/users/${id}`);
} catch (err) {
setError(err.normalized?.ui ?? 'Something went wrong.');
}
}
role="alert"
) for error messages; keep messages concise and actionable.GET
/PUT
and transient network/5xx errors. Use exponential backoff and cap attempts.status
, statusText
, a correlation ID, and minimal payload context; avoid PII in logs.Status class | Typical meaning | Suggested UI message |
---|---|---|
4xx | Client/auth errors | Check credentials/permissions; review your request. |
401 | Unauthenticated | Please log in to continue. |
403 | Unauthorized/forbidden | You do not have permission to perform this action. |
404 | Resource not found | The requested resource was not found. |
408/429 | Timeout / rate limited | Too many requests or timeout; try again later. |
5xx | Server/temporary outage | Server unavailable. Please try again shortly. |
TypeScript tips — strongly typed Axios errors
catch (err: unknown)
, use axios.isAxiosError<ApiError>(err)
to guard before reading Axios-specific fields.axios.get<User[]>('/users')
so res.data
is correctly typed.ApiError
interface to surface server messages without any
.import axios, { AxiosError } from 'axios';
interface User { id: number; name: string }
interface ApiError { message: string; code?: string }
async function loadUsers() {
try {
const res = await axios.get<User[]>('/users');
return res.data; // typed: User[]
} catch (err: unknown) {
if (axios.isAxiosError<ApiError>(err)) {
const status = err.response?.status;
const serverMsg = err.response?.data?.message;
// Present a friendly UI message; log diagnostics
throw new Error(serverMsg ?? `Request failed${status ? ` (${status})` : ''}`);
}
// Non-Axios/runtime error — rethrow for error boundary/telemetry
throw err;
}
}
Bottom line: Centralize Axios error handling for consistency, present user‑friendly messages, and keep diagnostic detail in logs. This balances developer velocity with a trustworthy user experience.
While Axios handles HTTP requests directly, React Query (now TanStack Query) abstracts data fetching, caching, and background updates for complex React apps.
import axios from "axios";
import { useQuery } from "@tanstack/react-query";
const fetchUsers = async () => {
const { data } = await axios.get("/api/users");
return data;
};
function UserList() {
const { data, error, isLoading } = useQuery(["users"], fetchUsers);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
This pattern gives you the best of both worlds: Axios’ request power and React Query’s caching + reactivity.
To build scalable, maintainable, and secure React applications, follow these best practices when working with Axios.
Interceptors allow you to attach authentication tokens (for example, JWT or OAuth2) and correlation IDs to every request. You can also log responses or normalize errors in one place, ensuring consistent behavior across the app.
useAxios
) for ReusabilityEncapsulate Axios logic in custom hooks to simplify your components. A useAxios
hook can handle loading, error, and retry states while returning data. This promotes cleaner components and allows you to reuse request logic throughout the project.
Keep your React components focused on UI. Place Axios calls in dedicated API modules (e.g., api.js
) or hooks, then import them into your components. This separation improves testability and makes it easier to swap out API implementations.
When choosing between Axios and the native Fetch API for making HTTP requests in React (or any JavaScript project), it’s important to consider your application’s complexity, maintainability needs, and developer experience.
Axios is a popular third-party library that simplifies HTTP requests with a concise syntax, automatic JSON parsing, and powerful features like interceptors, request cancellation, and robust error handling. Fetch is a modern, built-in browser API that provides a low-level interface for making HTTP requests, but requires more manual work for common tasks.
AbortController
)Feature | Axios | Fetch API |
---|---|---|
Syntax | Cleaner, promise-based with less boilerplate | More verbose, requires manual JSON parsing |
Error Handling | Auto throws on non-2xx responses | Must manually check response.ok |
Interceptors | Built-in request/response interceptors | Not available, requires custom wrappers |
JSON Handling | Auto-parses JSON | Requires explicit res.json() |
Request Cancellation | Built-in support (AbortController & cancel tokens) |
AbortController only, manual integration |
Timeout Support | Native support via config | Requires manual implementation |
Progress Events | Supported (browser, Node) | Not natively supported |
Uploads/Downloads | Easier with Axios | More manual with Fetch |
Browser Support | Works in all major browsers (with polyfill) | Modern browsers only |
Tip: For enterprise-scale apps, Axios saves development time with interceptors and error handling. For small apps or when bundle size matters, Fetch may be sufficient.
Integrating your API components into the main application is essential for building a practical, interactive React app. Below, you’ll see how to combine the PersonList
, PersonAdd
, and PersonRemove
components in App.js
for both class-based and hooks-based implementations. This approach demonstrates how to manage data listing, creation, and deletion together in a real-world React app, providing a full CRUD experience.
PersonList
, PersonAdd
, and PersonRemove
In this example, you import all three class-based components and render them in your App.js
. This setup allows users to add a person, view the list, and remove a person—all in one place. This pattern is common in dashboards and admin panels.
// src/App.js
import PersonList from './components/PersonList';
import PersonAdd from './components/PersonAdd';
import PersonRemove from './components/PersonRemove';
function App() {
return (
<div className="App">
<h2>Add a Person</h2>
<PersonAdd />
<h2>People List</h2>
<PersonList />
<h2>Remove a Person</h2>
<PersonRemove />
</div>
);
}
export default App;
Why this integration?
Combining create, read, and delete operations in a single app view allows users to interact with your API in a cohesive way. It also makes it easier to manage state and see the effects of each action in context.
PersonListHooks
, PersonAddHooks
, and PersonRemoveHooks
For modern React apps, hooks-based components are preferred for their simplicity and flexibility. Here, you import the hooks versions of your API components and render them together in App.js
. This pattern is ideal for new projects and teams adopting functional React.
// src/App.js
import PersonListHooks from './components/PersonListHooks';
import PersonAddHooks from './components/PersonAddHooks';
import PersonRemoveHooks from './components/PersonRemoveHooks';
function App() {
return (
<div className="App">
<h2>Add a Person</h2>
<PersonAddHooks />
<h2>People List</h2>
<PersonListHooks />
<h2>Remove a Person</h2>
<PersonRemoveHooks />
</div>
);
}
export default App;
Why use hooks-based integration?
Hooks components provide better state management, easier side-effect handling, and a more concise syntax. Integrating their hooks versions together in App.js
gives you a scalable and maintainable foundation for CRUD operations in your application.
You can also mix class and hooks components as needed, especially when migrating legacy code or adopting hooks incrementally. For example, you could use PersonAddHooks
with PersonList
and PersonRemoveHooks
:
// src/App.js
import PersonList from './components/PersonList';
import PersonAddHooks from './components/PersonAddHooks';
import PersonRemoveHooks from './components/PersonRemoveHooks';
function App() {
return (
<div className="App">
<PersonAddHooks />
<PersonList />
<PersonRemoveHooks />
</div>
);
}
export default App;
Takeaway:
App-level integration of your API components—whether class-based or hooks-based—enables a seamless, practical user experience. This approach mirrors real-world production apps, where you often need to combine multiple data operations in a single UI.
Centralize data fetching with a reusable hook that wraps a shared Axios instance. This pattern provides loading/error state, status codes, cancellation, and an imperative execute()
for on‑demand calls.
// src/hooks/useAxios.js
import { useCallback, useEffect, useRef, useState } from 'react';
import API from '../api'; // the configured axios instance
/**
* useAxios — generic Axios hook
* @param {Object} options - { url, method, data, params, headers, immediate }
* @returns {Object} { data, error, loading, status, execute, cancel }
*/
export default function useAxios({ url, method = 'get', data = undefined, params = undefined, headers = undefined, immediate = true } = {}) {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState(undefined);
const controllerRef = useRef(null);
const execute = useCallback(async (override = {}) => {
setLoading(true);
setError(null);
controllerRef.current?.abort();
controllerRef.current = new AbortController();
try {
const res = await API.request({
url,
method,
data,
params,
headers,
signal: controllerRef.current.signal,
...override,
});
setStatus(res.status);
setResponse(res.data);
return res;
} catch (err) {
// do not treat cancellation as an error
if (err.name === 'CanceledError') return;
setStatus(err.response?.status);
setError(err);
throw err;
} finally {
setLoading(false);
}
}, [url, method, data, params, headers]);
const cancel = useCallback(() => controllerRef.current?.abort(), []);
useEffect(() => {
if (immediate && url) execute();
return () => controllerRef.current?.abort();
}, [immediate, url, execute]);
return { data: response, error, loading, status, execute, cancel };
}
useAxios
// src/components/UsersWithHook.js
import React from 'react';
import useAxios from '../hooks/useAxios';
export default function UsersWithHook() {
const { data: users, loading, error, status, execute } = useAxios({ url: 'users' });
if (loading) return <p>Loading…</p>;
if (error) return <p role="alert">Failed ({status ?? 'N/A'}). Try again.</p>;
return (
<div>
<button onClick={() => execute()}>Reload</button>
<ul>
{(users || []).map(u => (<li key={u.id}>{u.name}</li>))}
</ul>
</div>
);
}
Why this helps: Consistent UX, fewer foot‑guns, and one place to evolve timeouts, headers, and interceptors as the app grows.
For more complex requirements in React projects, Axios offers powerful features such as pagination, concurrent requests, file uploads with progress, and advanced timeout control. Here are practical patterns for each:
Use query params and inspect response headers for pagination:
import API from './api';
async function fetchUsersPage(page = 1, limit = 5) {
const res = await API.get('users', { params: { _page: page, _limit: limit } });
// JSONPlaceholder sends total count in headers
const total = res.headers['x-total-count'];
console.log('Total users:', total);
return res.data;
}
Fetch users and posts at the same time:
import API from './api';
async function fetchUsersAndPosts() {
const [usersRes, postsRes] = await Promise.all([
API.get('users'),
API.get('posts'),
]);
return { users: usersRes.data, posts: postsRes.data };
}
Track upload progress using Axios’s onUploadProgress
:
import React, { useState } from 'react';
import API from './api';
function FileUploader() {
const [progress, setProgress] = useState(0);
const [file, setFile] = useState(null);
const handleChange = (e) => setFile(e.target.files[0]);
const handleUpload = async () => {
if (!file) return;
const form = new FormData();
form.append('file', file);
try {
await API.post('/upload', form, {
onUploadProgress: (evt) => {
if (evt.total) setProgress(Math.round((evt.loaded * 100) / evt.total));
},
});
alert('Upload complete!');
} catch (err) {
alert('Upload failed!');
}
};
return (
<div>
<input type="file" onChange={handleChange} />
<button onClick={handleUpload}>Upload</button>
{progress > 0 && <p>Progress: {progress}%</p>}
</div>
);
}
Set a custom timeout and handle timeouts gracefully:
import axios from 'axios';
const API = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com/',
timeout: 8000, // 8 seconds
});
async function fetchWithTimeout() {
try {
const res = await API.get('users');
return res.data;
} catch (err) {
if (err.code === 'ECONNABORTED') {
console.error('Request timed out!');
} else {
console.error('Other error:', err.message);
}
}
}
Tip: Use these advanced patterns to handle real-world requirements like infinite scroll, batch loading, file uploads, and robust network error handling in your React apps.
Why this matters (EEAT): React MCP (Model Context Protocol) provides a structured, declarative way to manage data models and server interactions in modern React apps. Pairing it with Axios ensures you retain fine‑grained network control (interceptors, error handling, retries) while leveraging MCP’s context‑driven data flow.
React MCP is an emerging open‑source protocol and server layer that standardizes how React components declare and consume remote data models. Instead of scattering REST or GraphQL calls across components, MCP lets you:
Repo: https://github.com/kalivaraprasad-gonapa/react-mcp
// src/mcp/index.js – register MCP context with Axios fetcher
import { createMCPClient } from 'react-mcp/client';
import API from '../api'; // your shared Axios instance
import '../api.interceptors'; // make sure interceptors are loaded once
const mcp = createMCPClient({
baseURL: 'https://jsonplaceholder.typicode.com/',
// Use Axios for all underlying HTTP calls so you keep interceptors/timeouts
fetcher: (cfg) => API.request(cfg),
});
export const MCPProvider = mcp.Provider; // Context provider
export const useModel = mcp.useModel; // Hook to subscribe to a model
// src/App.js – wrap your app in the MCP provider
import { MCPProvider } from './mcp';
import PersonListHooks from './components/PersonListHooks';
function App() {
return (
<MCPProvider>
<PersonListHooks />
</MCPProvider>
);
}
// src/components/PersonListMCP.js – declarative data slice
import { useModel } from '../mcp';
export default function PersonListMCP() {
// The first arg is the server‑side *model name*; second is query/filter params
const { data: users, loading, error, refetch } = useModel('users', { limit: 10 });
if (loading) return <p>Loading users…</p>;
if (error) return <p role="alert">{error.message}</p>;
return (
<div>
<button onClick={refetch}>Reload</button>
<ul>
{users.map((u) => <li key={u.id}>{u.name}</li>)}
</ul>
</div>
);
}
fetcher
so interceptors (auth, retry, telemetry) apply everywhere.createUser
), MCP automatically invalidates users
queries — no manual cache busting.X‑Request‑ID
for end‑to‑end tracing in distributed systems.Result: You keep Axios’ low‑level power (timeouts, cancellation) while gaining MCP’s declarative data layer — a future‑proof architecture for large React apps.
Q1: How do I install Axios in a React project?
Install with your package manager and import it where needed. Prefer a shared instance for consistency.
npm install axios # or: yarn add axios
import axios from 'axios';
// or centralize config
// import API from './api';
See the official Axios GitHub docs for options like timeouts and headers.
Q2: How do I make a GET request with Axios in React?
Call axios.get(url)
(or API.get(path)
from a configured instance) inside useEffect
and update state with the response. Always handle errors and consider cancellation for unmounts.
useEffect(() => {
const controller = new AbortController();
axios.get('/users', { signal: controller.signal })
.then(r => setData(r.data))
.catch(console.error);
return () => controller.abort();
}, []);
Q3: How do I make a POST request with Axios in React?
Use axios.post(url, payload)
with async/await
. Log res.status
for visibility and include headers for JSON or auth when needed.
try {
const res = await axios.post('/users', { name });
console.log(res.status, res.data);
} catch (err) {
// handle err.response / err.request / err.message
}
A configured instance helps apply common headers automatically.
Q4: How do I handle errors with Axios in React?
Wrap calls in try/catch
and branch on err.response
(HTTP status), err.request
(no response), and err.message
(setup/timeouts). Show friendly UI text, log diagnostics privately, and cancel inflight requests on unmount. See the How to Handle Errors section for a production pattern and status-to-message mapping.
Q5: What are Axios interceptors and how do I use them in React?
Interceptors run before requests and after responses. Use a shared instance to attach auth tokens, correlation IDs, and normalize errors/telemetry. Example: add Authorization: Bearer <token>
in a request interceptor and retry once on 401
in a response interceptor. Register them once and import the instance across components.
Q6: Should I use Axios or Fetch in React?
Both work. Axios reduces boilerplate, auto-parses JSON, and supports interceptors/cancellation—great for larger apps. Fetch is built-in and minimal, fine for small utilities. See the Axios vs Fetch comparison table above for syntax, error handling, JSON parsing, and cancellation differences to choose confidently.
Q7: Can I use Axios with async/await in React?
Yes. async/await
keeps control flow top‑to‑bottom and makes errors explicit via try/catch
. It pairs naturally with hooks like useEffect
and useState
for readable data fetching.
try {
const res = await API.delete(`/users/${id}`);
setResult(res.data);
} catch (err) {
// status‑aware handling here
}
Q8: How can I cancel Axios requests in React?
Pass an AbortController
signal when making the request and call controller.abort()
in the effect cleanup or when a user navigates away. This prevents state updates on unmounted components and saves bandwidth—crucial for lists, search-as-you-type, and rapid route changes.
const controller = new AbortController();
axios.get('/users', { signal: controller.signal });
// later
controller.abort();
When building AI-driven applications—such as React dashboards that interact with large language models (LLMs) or other AI APIs—Axios becomes indispensable for reliability and scalability. AI endpoints often require telemetry for observability, automatic retries for transient failures, and robust rate-limit handling to avoid disruptions. By leveraging Axios interceptors and global instances, you can consistently manage authentication tokens, implement retry logic, and centralize error handling across your app. This ensures secure, traceable, and resilient communication with AI providers, enabling your production LLM applications to gracefully handle API quotas, network hiccups, and evolving authentication schemes as you scale.
In this tutorial, you learned how to use Axios with React by walking through practical, real-world scenarios. You covered:
By applying these patterns, you’ll write React applications that are more reliable, maintainable, and secure.
If you’d like to deepen your React knowledge, explore the How To Code in React.js series for more examples and projects.
Want to deploy your React project live? Try DigitalOcean App Platform and push your React app from GitHub to production in minutes.
Expand your knowledge and stay up to date with these resources:
React Documentation — Learn React
The official React docs, including tutorials and guides for all experience levels.
Axios GitHub Documentation
The source of truth for Axios usage, configuration, and advanced features.
MDN — AbortController
Learn how to use AbortController to cancel HTTP requests in modern JavaScript.
DigitalOcean — JS Fetch API Guide
A practical introduction to the Fetch API, including examples and best practices.
DigitalOcean — How To Code in React.js Series
A step-by-step series covering React fundamentals, component patterns, and real-world projects.
Tip: For even deeper dives, consider exploring topics like React Query, TanStack Query, or advanced error handling patterns with Axios. The React and Axios communities are active and regularly publish new guides and best practices.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
I create educational content over at YouTube and https://developer.school.
Building future-ready infrastructure with Linux, Cloud, and DevOps. Full Stack Developer & System Administrator @ DigitalOcean | GitHub Contributor | Passionate about Docker, PostgreSQL, and Open Source | Exploring NLP & AI-TensorFlow | Nailed over 50+ deployments across production environments.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Great article! I saw it’s using class component. Are there any updates to use the newer react version that uses functional component?
For the last part, isn’t “await” need to use inside async function? Sorry I’m a javascript newbie…
For the last part, isn’t “await” need to use inside async function? Sorry I’m a javascript newbie…
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.