The useEffect Hook does not accept an async function directly, because its setup must return either nothing or a cleanup function. The setup may return undefined or a function, never a Promise. The pattern for async functions inside useEffect is to declare one, call it, and guard against stale responses in the cleanup.
Why the setup cannot be async
React expects the Effect setup to return undefined or a cleanup function. An async function always returns a Promise, which React cannot use as cleanup.
useEffect(async () => {
const data = await fetchData();
setData(data);
}, []);This looks convenient but it is wrong. The Promise becomes the Effect's return value, React has no cleanup to run, and any state update after the component unmounts still fires. React also needs to run the setup and cleanup as a pair, and an async function hides the cleanup entirely.
Declare, call, and clean up
Move the async work into a function declared inside the Effect. Call it, and return a cleanup that marks the result as stale. The outer setup stays synchronous and returns the cleanup, while all await work moves into load.
import { useEffect, useState } from "react";
function Profile({ userId }) {
const [profile, setProfile] = useState(null);
useEffect(() => {
let ignore = false;
(async () => {
const data = await (await fetch(`/api/users/${userId}`)).json();
if (!ignore) setProfile(data);
})();
return () => { ignore = true; };
}, [userId]);
return <p>{profile?.name ?? "Loading..."}</p>;
}The page shows a loading label, then the fetched name. When userId changes, the cleanup flips the old Effect's ignore flag, so a slow response from the previous user is dropped instead of overwriting the new one. The ignore flag matters because the outer setup runs again whenever userId changes.
The same pattern works for any async source: a database read, a file, or a delayed task. Declare the async function, call it, and flip the flag in the cleanup. The shape stays the same across all of them.
Handle errors inside the async function
A rejected Promise inside an async function throws at the await. Catch it so the failure becomes state instead of an unhandled rejection. The catch runs in the same closure, so it can check the same ignore flag and keep a stale error out of state.
async function load() {
try {
const data = await fetchData(userId);
if (!ignore) setData(data);
} catch (error) {
if (!ignore) setError(error);
}
}The UI can now render an error message alongside the loading and success states. The same ignore check keeps an error from a stale request out of state. Without try/catch, the rejected Promise becomes an unhandled rejection and the loading label stays forever.
What to learn next
The ignore flag is the core of fetching without race conditions. Cleaning up timers and listeners covers the same return-cleanup shape for other work, and the dependency array decides when the async work should restart.
Rune AI
Key Insights
- Never pass an async function directly to useEffect.
- Declare an async function inside the Effect and call it.
- Use a try/catch block inside the async function to capture errors.
- Use an ignore flag in the cleanup to drop stale responses.
Frequently Asked Questions
Can I pass an async function directly to useEffect?
Where should the async logic go?
How do I handle errors from async work?
Conclusion
useEffect needs a synchronous setup or a cleanup function, so async work goes inside a nested function that you call. Pair it with a cleanup that ignores stale results and you get the standard pattern.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.