How to Call an Async Function in React
What you’ll build or solve
You’ll call async functions in the two places React expects side effects: effects and event handlers.
When this approach works best
Calling an async function in React works best when you:
Learn React on Mimo
- Fetch data when a component loads.
- Refetch data when props or state change.
- Submit a form and wait for a server response.
- Trigger network calls from clicks or other user actions.
Avoid calling async code during rendering. Rendering must stay synchronous and free of side effects.
Prerequisites
- Basic React knowledge
- Understanding of
useStateanduseEffect - Familiarity with
asyncandawait
Step-by-step instructions
Step 1: Call async functions in useEffect for lifecycle work
Use useEffect for async work that should run after render, like loading data on mount or when dependencies change.
You cannot make the effect callback async. Define an async function inside the effect, then call it.
JavaScript
import { useEffect, useState } from "react";
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
async function fetchUsers() {
const response = await fetch("/api/users");
const data = await response.json();
setUsers(data);
}
fetchUsers();
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
What to look for:
- The async function lives inside
useEffect. - The dependency array controls when it runs.
- Add dependencies when the request depends on props or state.
Step 2: Call async functions in event handlers for user actions
Event handlers can be async directly. Use this for clicks, form submissions, and other user actions.
JavaScript
import { useState } from "react";
function SaveButton() {
const [status, setStatus] = useState("idle");
const handleClick = async () => {
setStatus("loading");
await fetch("/api/save", { method: "POST" });
setStatus("done");
};
return (
<button onClick={handleClick}>
{status === "loading" ? "Saving..." : "Save"}
</button>
);
}
What to look for:
- Put
awaitinside the handler, not during render. - Update state after the request finishes.
Examples you can copy
Example 1: Fetch on mount with error handling
JavaScript
useEffect(() => {
async function loadProducts() {
try {
const res = await fetch("/api/products");
const products = await res.json();
setProducts(products);
} catch (error) {
setError("Failed to load products");
}
}
loadProducts();
}, []);
Example 2: Fetch when a prop changes
JavaScript
useEffect(() => {
async function loadUser() {
const res = await fetch(`/api/users/${userId}`);
const user = await res.json();
setUser(user);
}
loadUser();
}, [userId]);
Example 3: Submit a form with async and await
JavaScript
const handleSubmit = async (event) => {
event.preventDefault();
try {
await fetch("/api/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData)
});
setSubmitted(true);
} catch (error) {
setError("Submit failed");
}
};
Example 4: Run multiple requests in parallel
JavaScript
useEffect(() => {
async function loadAll() {
const [usersRes, postsRes] = await Promise.all([
fetch("/api/users"),
fetch("/api/posts")
]);
const users = await usersRes.json();
const posts = await postsRes.json();
setUsers(users);
setPosts(posts);
}
loadAll();
}, []);
Common mistakes and how to fix them
Mistake 1: Making useEffect directly async
What you might do:
JavaScript
useEffect(async () => {
const res = await fetch("/api/data");
const data = await res.json();
setData(data);
}, []);
Why it breaks:
React expects the effect callback to return nothing or a cleanup function, not a Promise.
Correct approach:
JavaScript
useEffect(() => {
async function load() {
const res = await fetch("/api/data");
const data = await res.json();
setData(data);
}
load();
}, []);
Mistake 2: Calling async code during render
What you might do:
JavaScript
const res = await fetch("/api/data");
Why it breaks:
Components must render synchronously. Async work belongs in effects or event handlers.
Correct approach:
Move the async call into useEffect or an event handler.
Mistake 3: Missing the dependency array
What you might do:
JavaScript
useEffect(() => {
async function load() {
const res = await fetch("/api/data");
setData(await res.json());
}
load();
});
Why it breaks:
Without dependencies, the effect runs after every render.
Correct approach:
JavaScript
useEffect(() => {
async function load() {
const res = await fetch("/api/data");
setData(await res.json());
}
load();
}, []);
Troubleshooting
- If an effect runs repeatedly, check the dependency array.
- If requests fail silently, wrap the async code in
try/catchand store the error in state. - If you see state updates after navigating away, guard updates in the effect with a cleanup flag.
- If data is
undefined, logresponse.statusand confirm the endpoint returns JSON.
Quick recap
- Call async functions in
useEffectfor lifecycle work. - Call async functions in event handlers for user actions.
- Do not call async code during render.
- Use dependencies to control when effects run.
- Add
try/catchand cleanup guards when needed.
Join 35M+ people learning for free on Mimo
4.8 out of 5 across 1M+ reviews
Check us out on Apple AppStore, Google Play Store, and Trustpilot