React Async and Await
Table of Contents + −
In the last lesson, React Fetch API, you fetched data with .then chains. Here we rewrite that same fetch with async and await so it reads top to bottom, and we cover the one tricky part of using it inside useEffect.
🤔 Why async and await?
The .then chains work, but they get tangled as the logic grows. Here’s the pain.
- Each step is a callback inside another callback, so the code drifts to the right.
- The order you read it isn’t quite the order things happen.
- Error handling with
.catchsits at the bottom, far from the line that might fail. - More steps mean more arrows, and it gets harder to follow.
So async/await lets you write asynchronous code that reads like normal, step-by-step code. Same behavior, much easier to follow.
🧩 The same fetch, two ways
Let’s see them side by side. Here’s the .then version you already know, then the async/await version that does the exact same thing.
// the .then wayfunction loadUsers() { fetch("https://jsonplaceholder.typicode.com/users") .then((response) => response.json()) .then((data) => console.log(data));}
// the async/await way - same result, reads top to bottomasync function loadUsers() { const response = await fetch("https://jsonplaceholder.typicode.com/users"); const data = await response.json(); console.log(data);}Read the async version line by line.
asyncin front of the function marks it as asynchronous, which lets you useawaitinside.awaitpauses on that line until the promise finishes, then gives you the result.- So
await fetch(...)waits for the response, andawait response.json()waits for the parsed data. - No callbacks, no nesting, just plain lines in the order they happen.
await only works inside async
You can only use await inside a function marked async. Try it in a normal function and you’ll get a syntax error. The two always go together.
🛡️ Add try/catch for errors
With .then you used .catch for failures. With async/await, you wrap the code in a try/catch block instead, which keeps the error handling right next to the code.
async function loadUsers() { try { const response = await fetch("https://jsonplaceholder.typicode.com/users"); const data = await response.json(); console.log(data); } catch (error) { console.log("Something went wrong:", error); }}Here’s how the two blocks split the work.
- The
tryblock holds the normal path, the steps you hope succeed. - If any
awaitin there fails, JavaScript jumps straight tocatch. - The
catchblock gets theerrorso you can handle it, like showing a message. - So success and failure are both right there, easy to read together.
💡 Using async/await inside useEffect
Now the one part that trips everyone up. You cannot make the useEffect callback itself async, so you define an async function inside the effect and call it.
import { useState, useEffect } from "react";
function UserList() { const [users, setUsers] = useState([]);
useEffect(() => { // define an async function inside the effect async function loadUsers() { try { const response = await fetch("https://jsonplaceholder.typicode.com/users"); const data = await response.json(); setUsers(data); } catch (error) { console.log("Failed to load users:", error); } }
loadUsers(); // then call it }, []);
return ( <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> );}Look at the pattern carefully, because it’s the standard one.
- We define
loadUsersas anasyncfunction inside the effect. - Inside it, we
awaitthe fetch and the JSON, then save the data withsetUsers. - After defining it, we call
loadUsers()on its own line. - The effect callback itself stays a normal function, not
async.
Don't make the effect callback async
Writing useEffect(async () => {...}) looks tempting but it’s wrong. An async function returns a promise, and React expects the effect to return either nothing or a cleanup function, not a promise. So define the async function inside and call it.
🔍 Why not make the effect itself async?
It helps to know the real reason, so the rule sticks.
useEffectmay return a cleanup function, and React runs that on unmount.- An
asyncfunction always returns a promise, never your cleanup function. - So React would get a promise where it expected cleanup, and your cleanup would never run.
- That’s why the inside-function pattern isn’t just style. It keeps the effect’s return value correct.
⚠️ Common Mistakes
The top mistake is forgetting await, so you work with a promise instead of the real value.
// ❌ no await - data is a Promise, not your arrayconst data = response.json();console.log(data.length); // undefined or wrong
// ✅ await it to get the actual valueconst data = await response.json();console.log(data.length); // worksKeep these in mind.
- Don’t forget
await. Without it you hold a promise, not the value, and things look “empty” or undefined. - Don’t use
awaitoutside anasyncfunction. It’s a syntax error. - Don’t make the
useEffectcallbackasync. Define the async function inside and call it.
✅ Best Practices
A few habits for clean async code in React.
- Prefer
async/awaitover long.thenchains. It reads top to bottom and is easier to follow. - Wrap your awaits in
try/catchso failures are handled right next to the code. - Inside
useEffect, define anasyncfunction and then call it. awaitevery promise whose result you actually need before using it.
Same fetch, nicer to read
async/await doesn’t change what your code does, it just changes how it looks. Under the hood it’s still promises. Pick whichever reads clearer to you, but most people find async/await easier once there’s more than one step.
🧩 What You’ve Learned
- ✅
async/awaitwrites the same async logic as.then, but it reads top to bottom - ✅
awaitpauses until a promise finishes and gives you its result - ✅
awaitonly works inside a function markedasync - ✅ Use
try/catchfor errors instead of.catch, keeping handling next to the code - ✅ Don’t make the
useEffectcallbackasync; define an async function inside and call it - ✅ Forgetting
awaitleaves you holding a promise instead of the real value
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
What does the await keyword do?
Why: await waits for a promise to settle and hands you its resolved value, so the next line can use the real data instead of a promise.
- 2
Where can you use the await keyword?
Why: await only works inside an async function. Using it in a normal function is a syntax error, which is why async and await always go together.
- 3
How do you use async/await inside useEffect correctly?
Why: The effect callback must not be async, because that returns a promise instead of a cleanup function. Define an async function inside the effect and then call it.
- 4
How do you handle errors with async/await?
Why: With async/await you wrap the code in try/catch. If any await fails, control jumps to the catch block where you handle the error.
🚀 What’s Next?
Now your fetch code is clean and readable. But a real request takes time, and right now the user sees nothing while they wait. Next you’ll add a loading state so the screen says “Loading…” until the data arrives.