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:

  • 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 useState and useEffect
  • Familiarity with async and await

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.

import {useEffect,useState }from"react";

functionUsers() {
const [users,setUsers]=useState([]);

useEffect(() => {
asyncfunctionfetchUsers() {
constresponse=awaitfetch("/api/users");
constdata=awaitresponse.json();
setUsers(data);
    }

fetchUsers();
  }, []);

return (
<ul>
      {users.map(user => (
<likey={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.

import {useState }from"react";

functionSaveButton() {
const [status,setStatus]=useState("idle");

consthandleClick=async () => {
setStatus("loading");
awaitfetch("/api/save", { method:"POST" });
setStatus("done");
  };

return (
<buttononClick={handleClick}>
      {status==="loading"?"Saving...":"Save"}
</button>
  );
}

What to look for:

  • Put await inside the handler, not during render.
  • Update state after the request finishes.

Examples you can copy

Example 1: Fetch on mount with error handling

useEffect(() => {
asyncfunctionloadProducts() {
try {
constres=awaitfetch("/api/products");
constproducts=awaitres.json();
setProducts(products);
    }catch (error) {
setError("Failed to load products");
    }
  }

loadProducts();
}, []);

Example 2: Fetch when a prop changes

useEffect(() => {
asyncfunctionloadUser() {
constres=awaitfetch(`/api/users/${userId}`);
constuser=awaitres.json();
setUser(user);
  }

loadUser();
}, [userId]);

Example 3: Submit a form with async and await

consthandleSubmit=async (event) => {
event.preventDefault();

try {
awaitfetch("/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

useEffect(() => {
asyncfunctionloadAll() {
const [usersRes,postsRes]=awaitPromise.all([
fetch("/api/users"),
fetch("/api/posts")
    ]);

constusers=awaitusersRes.json();
constposts=awaitpostsRes.json();

setUsers(users);
setPosts(posts);
  }

loadAll();
}, []);

Common mistakes and how to fix them

Mistake 1: Making useEffect directly async

What you might do:

useEffect(async () => {
constres=awaitfetch("/api/data");
constdata=awaitres.json();
setData(data);
}, []);

Why it breaks:

React expects the effect callback to return nothing or a cleanup function, not a Promise.

Correct approach:

useEffect(() => {
asyncfunctionload() {
constres=awaitfetch("/api/data");
constdata=awaitres.json();
setData(data);
  }

load();
}, []);

Mistake 2: Calling async code during render

What you might do:

constres=awaitfetch("/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:

useEffect(() => {
asyncfunctionload() {
constres=awaitfetch("/api/data");
setData(awaitres.json());
  }

load();
});

Why it breaks:

Without dependencies, the effect runs after every render.

Correct approach:

useEffect(() => {
asyncfunctionload() {
constres=awaitfetch("/api/data");
setData(awaitres.json());
  }

load();
}, []);

Troubleshooting

  • If an effect runs repeatedly, check the dependency array.
  • If requests fail silently, wrap the async code in try/catch and 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, log response.status and confirm the endpoint returns JSON.

Quick recap

  • Call async functions in useEffect for 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/catch and cleanup guards when needed.