← Back to React Course | Chapter 10: Data Fetching & Async | Lesson 3 of 10

async/await Inside useEffect

Using async/await inside useEffect is like sending a helper to fetch coffee and telling them to come straight back and report, instead of leaving them a note they might read whenever.

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

markup
<!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

markup
<!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

markup
<!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>
Common Mistakes
  1. Making the useEffect callback itself async, which React doesn't support — the callback must return either nothing or a cleanup function, not a Promise.
  2. Forgetting to guard against setting state after the component unmounts during a slow async call.
  3. Not handling the rejected case of the awaited promise, letting errors disappear silently.
Chapter Summary
  • 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.
Browser Support

async/await and useEffect are both broadly supported (ES2017+ syntax, React 16.8+ Hooks).

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.