React Handling Promises & Async/Await
When building a React application, one of the most common tasks developers face is fetching data from APIs. Whether you’re loading user profiles, products, or posts, you’ll often need to handle asynchronous operations efficiently.
In JavaScript, this is done using Promises or async/await — two powerful ways to manage asynchronous data fetching. Understanding both will help you write cleaner, faster, and more reliable React components.
What Is Asynchronous Data Fetching?
When React components need data from an API, the request doesn’t complete instantly. If React waited for the response before rendering, your app would freeze — and that’s where asynchronous code comes in.
With Promises or async/await, your component can continue rendering while the data loads in the background.
What Is a Promise?
A Promise in JavaScript represents the result of an operation that hasn’t completed yet — it will either resolve successfully or reject with an error later.
Example:
fetch("<https://jsonplaceholder.typicode.com/posts>")
.then(response => response.json())
.then(data => console.log("Fetched Data:", data))
.catch(error => console.error("Error:", error));
Here:
fetch()returns a Promise..then()handles the resolved value..catch()handles any errors.
This approach works well, but when there are multiple async calls or error checks, .then() chains can become hard to read.
Using Promises in React Components
In React, the best place to run side effects like API calls is the useEffect hook.
Example:
import React, { useEffect, useState } from "react";
function Posts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("<https://jsonplaceholder.typicode.com/posts>")
.then(res => res.json())
.then(data => {
setPosts(data);
setLoading(false);
})
.catch(err => console.error("Error:", err));
}, []);
if (loading) return <p>Loading...</p>;
return (
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
export default Posts;
The Modern Way – Async/Await
The async/await syntax makes asynchronous code look synchronous, improving readability and flow.
Example:
import React, { useEffect, useState } from "react";
function PostsAsync() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await fetch("<https://jsonplaceholder.typicode.com/posts>");
if (!response.ok) throw new Error("Failed to fetch");
const data = await response.json();
setPosts(data);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoading(false);
}
};
fetchPosts();
}, []);
if (loading) return <p>Loading...</p>;
return (
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
export default PostsAsync;
Why use async/await in React?
- Cleaner and easier to debug
- Natural
try...catcherror handling - Works perfectly with hooks like
useEffect - Makes complex logic more readable
Handling Multiple Async Calls
You can run multiple API calls simultaneously using Promise.all():
const [usersRes, postsRes] = await Promise.all([
fetch("/api/users"),
fetch("/api/posts")
]);
const users = await usersRes.json();
const posts = await postsRes.json();
This approach improves performance since both requests happen at the same time.
Example: Custom Hook for Async Data Fetching
import { useState, useEffect } from "react";
export function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error("Network error");
const json = await res.json();
setData(json);
} catch (err) {
if (err.name !== "AbortError") setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
You can now reuse it in any component:
function Users() {
const { data, loading, error } = useFetch("<https://jsonplaceholder.typicode.com/users>");
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
