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.

JavaScript

import { useEffect, useState } from "react";

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

  useEffect(() => {
    async function fetchUsers() {
      const response = await fetch("/api/users");
      const data = await response.json();
      setUsers(data);
    }

    fetchUsers();
  }, []);

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

JavaScript

import { useState } from "react";

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

  const handleClick = async () => {
    setStatus("loading");
    await fetch("/api/save", { method: "POST" });
    setStatus("done");
  };

  return (
    <button onClick={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

JavaScript

useEffect(() => {
  async function loadProducts() {
    try {
      const res = await fetch("/api/products");
      const products = await res.json();
      setProducts(products);
    } catch (error) {
      setError("Failed to load products");
    }
  }

  loadProducts();
}, []);

Example 2: Fetch when a prop changes

JavaScript

useEffect(() => {
  async function loadUser() {
    const res = await fetch(`/api/users/${userId}`);
    const user = await res.json();
    setUser(user);
  }

  loadUser();
}, [userId]);

Example 3: Submit a form with async and await

JavaScript

const handleSubmit = async (event) => {
  event.preventDefault();

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

JavaScript

useEffect(() => {
  async function loadAll() {
    const [usersRes, postsRes] = await Promise.all([
      fetch("/api/users"),
      fetch("/api/posts")
    ]);

    const users = await usersRes.json();
    const posts = await postsRes.json();

    setUsers(users);
    setPosts(posts);
  }

  loadAll();
}, []);

Common mistakes and how to fix them

Mistake 1: Making useEffect directly async

What you might do:

JavaScript

useEffect(async () => {
  const res = await fetch("/api/data");
  const data = await res.json();
  setData(data);
}, []);

Why it breaks:

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

Correct approach:

JavaScript

useEffect(() => {
  async function load() {
    const res = await fetch("/api/data");
    const data = await res.json();
    setData(data);
  }

  load();
}, []);

Mistake 2: Calling async code during render

What you might do:

JavaScript

const res = await fetch("/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:

JavaScript

useEffect(() => {
  async function load() {
    const res = await fetch("/api/data");
    setData(await res.json());
  }

  load();
});

Why it breaks:

Without dependencies, the effect runs after every render.

Correct approach:

JavaScript

useEffect(() => {
  async function load() {
    const res = await fetch("/api/data");
    setData(await res.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.