async/await Inside useEffect
In this page:
Why useEffect's Callback Can't Be async Directly
useEffect expects its callback to return either nothing, or a cleanup function. An async function always returns a Promise, which React can't interpret as a valid cleanup function, so making the effect callback itself async triggers a warning and won't work correctly.
Note: The fix is simple: define a separate async function inside the effect, then call it immediately, keeping the outer effect callback itself synchronous.
Warning: Writing useEffect(async () => {...}) looks like it should work but produces a React warning about an effect returning a Promise instead of a cleanup function.
Example: Why useEffect's Callback Can't Be async Directly
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
async function load() {
const result = "Loaded via inner async function";
setData(result);
}
load();
}, []);
return <p>{data || "Loading..."}</p>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Handling Errors with try/catch
Since the inner async function uses await, wrapping the awaited calls in try/catch lets you handle rejected promises (like a failed fetch) in the same place, setting an error state the component can display, instead of letting the rejection disappear unnoticed.
Note: Catch errors as close to where they happen as possible — inside the inner async function, not the outer effect.
Warning: Without try/catch, a rejected awaited promise becomes an unhandled promise rejection, invisible to the user and easy to miss during development.
Example: Handling Errors with try/catch
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
const [error, setError] = React.useState(null);
React.useEffect(() => {
async function load() {
try {
throw new Error("Simulated failure");
} catch (err) {
setError(err.message);
}
}
load();
}, []);
return <p style={{color: "red"}}>{error || "No errors"}</p>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Guarding Against Updates After Unmount
If the component unmounts before a slow async call finishes, calling setState afterward logs a React warning. A boolean flag, set to true in the effect's cleanup function, lets the async function check whether it's still safe to update state before doing so.
Note: Check the cancelled flag right before each setState call inside the async function, not just once at the top.
Warning: Skipping this guard is mostly harmless for fast requests, but becomes a real issue for slow ones on pages users navigate away from quickly.
Example: Guarding Against Updates After Unmount
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
let cancelled = false;
async function load() {
const result = "Safely loaded";
if (!cancelled) setData(result);
}
load();
return () => { cancelled = true; };
}, []);
return <p>{data || "Loading..."}</p>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
- Making the useEffect callback itself async, which React doesn't support — the callback must return either nothing or a cleanup function, not a Promise.
- Forgetting to guard against setting state after the component unmounts during a slow async call.
- Not handling the rejected case of the awaited promise, letting errors disappear silently.
- useEffect's callback function cannot be declared async directly — React expects it to return void or a cleanup function.
- The standard workaround is defining an async function INSIDE the effect and calling it immediately.
- try/catch around the awaited calls handles errors that a plain .then() chain might silently drop.
- A cancelled flag guards against updating state after the component has unmounted.
async/await and useEffect are both broadly supported (ES2017+ syntax, React 16.8+ Hooks).
Chapter Quiz — Complete all 10 topics to unlock
0/10 topics done
Complete these topics first: