📘 ReactJS

Promises, async/await & API Calls

React applications commonly communicate with backend APIs. To understand API calls in React, you need a solid understanding of JavaScript asynchronous programming, especially Promises and async/await.

S

Site Administrator

📅 Sep 27, 2026 🕒 5 min read 👁 23 views
☆ Save 𝕏 in f ✆
Promises, async/await & API Calls

React applications commonly communicate with backend APIs. To understand API calls in React, you need a solid understanding of JavaScript asynchronous programming, especially Promises and async/await.

1 Synchronous vs Asynchronous

Synchronous code generally runs one operation after another. Asynchronous code allows a long-running operation to complete later without blocking the JavaScript execution flow in the same way.

Network requests are a common example of asynchronous work.

2 What Is a Promise?

A Promise represents the eventual result of an asynchronous operation. A Promise can be pending, fulfilled, or rejected.

flowchart TD
A[Promise Created] --> B[Pending]
B --> C[Fulfilled]
B --> D[Rejected]

3 Basic Promise

const promise = new Promise(
    (resolve, reject) => {
        const success = true;

        if (success) {
            resolve("Operation completed");
        } else {
            reject("Operation failed");
        }
    }
);

4 then and catch

promise
    .then(result => {
        console.log(result);
    })
    .catch(error => {
        console.error(error);
    });

then() handles a successful result. catch() handles a rejection.

5 fetch()

The browser provides the Fetch API for making HTTP requests.

fetch("/api/users")
    .then(response => response.json())
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.error(error);
    });

6 What Does fetch Return?

fetch() returns a Promise for a Response object. The response body must then be read, for example with response.json().

fetch("/api/users")
    .then(response => {
        return response.json();
    })
    .then(data => {
        console.log(data);
    });

7 async and await

async and await provide a cleaner syntax for working with Promises.

async function loadUsers() {
    const response =
        await fetch("/api/users");

    const data =
        await response.json();

    return data;
}

An async function always returns a Promise.

8 Error Handling with try/catch

async function loadUsers() {
    try {
        const response =
            await fetch("/api/users");

        if (!response.ok) {
            throw new Error(
                "Request failed"
            );
        }

        const data =
            await response.json();

        return data;
    }
    catch (error) {
        console.error(error);
    }
}

9 Promise vs async/await

Promisesasync/await
Uses then/catchLooks more like synchronous code
Useful for chainingOften easier for sequential operations
Still fundamental underneathBuilt on top of Promises

10 API Calls in React

When a component needs data from an API after rendering, React commonly uses an effect to start the request.

import { useEffect, useState } from "react";

function Users() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState("");

    useEffect(() => {
        async function loadUsers() {
            try {
                const response =
                    await fetch("/api/users");

                if (!response.ok) {
                    throw new Error(
                        "Unable to load users"
                    );
                }

                const data =
                    await response.json();

                setUsers(data);
            }
            catch (error) {
                setError(error.message);
            }
            finally {
                setLoading(false);
            }
        }

        loadUsers();
    }, []);

    if (loading) {
        return <p>Loading...</p>;
    }

    if (error) {
        return <p>{error}</p>;
    }

    return (
        <ul>
            {users.map(user => (
                <li key={user.id}>
                    {user.name}
                </li>
            ))}
        </ul>
    );
}

11 API Call Flow in React

flowchart TD
A[Component Renders] --> B[Effect Starts]
B --> C[API Request]
C --> D{Response}
D -->|Success| E[Parse JSON]
D -->|Failure| F[Handle Error]
E --> G[Update State]
F --> H[Set Error State]
G --> I[Render Data]
H --> J[Render Error]

12 HTTP Status Check

A common mistake is assuming that fetch() rejects automatically for every HTTP error. For HTTP responses such as 404 or 500, you should normally check response.ok or the status code.

const response =
    await fetch("/api/users");

if (!response.ok) {
    throw new Error(
        `HTTP error: ${response.status}`
    );
}

13 POST Request

async function createUser(user) {
    const response = await fetch(
        "/api/users",
        {
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            },
            body: JSON.stringify(user)
        }
    );

    if (!response.ok) {
        throw new Error(
            "Unable to create user"
        );
    }

    return response.json();
}

14 Loading, Error, and Data States

A real API component normally needs at least three states:

  • Loading
  • Error
  • Data

There may also be an empty-data state.

flowchart TD
A[Start Request] --> B[Loading]
B --> C{Request Result}
C -->|Success With Data| D[Show Data]
C -->|Success Empty| E[Show Empty State]
C -->|Failure| F[Show Error]

15 Avoiding an async Effect Callback

For a simple effect, instead of making the effect callback itself async, define an async function inside it and call that function.

useEffect(() => {
    async function loadData() {
        const response =
            await fetch("/api/data");

        const data =
            await response.json();

        setData(data);
    }

    loadData();
}, []);

16 Complete Example — Search by User

function UserSearch() {
    const [search, setSearch] = useState("");
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState("");

    async function searchUsers() {
        setLoading(true);
        setError("");

        try {
            const response = await fetch(
                `/api/users?search=${encodeURIComponent(search)}`
            );

            if (!response.ok) {
                throw new Error(
                    "Unable to search users"
                );
            }

            const data = await response.json();

            setUsers(data);
        }
        catch (error) {
            setError(error.message);
        }
        finally {
            setLoading(false);
        }
    }

    return (
        <div>
            <input
                value={search}
                onChange={event =>
                    setSearch(event.target.value)
                }
            />

            <button onClick={searchUsers}>
                Search
            </button>

            {loading && <p>Loading...</p>}

            {error && <p>{error}</p>}

            {users.map(user => (
                <p key={user.id}>
                    {user.name}
                </p>
            ))}
        </div>
    );
}

17 Common Mistakes

  • Forgetting that fetch() returns a Promise.
  • Forgetting to call response.json() for JSON responses.
  • Not checking response.ok.
  • Ignoring loading and error states.
  • Making the effect callback itself async without understanding the cleanup contract.
  • Updating state after a component is no longer interested in the request result.
  • Starting API requests repeatedly because the effect dependencies are incorrect.

18 Interview Questions

  1. What is a Promise?
  2. What are the states of a Promise?
  3. What is the difference between then/catch and async/await?
  4. What does an async function return?
  5. Does fetch reject automatically for HTTP 404 or 500?
  6. Why do we call response.json()?
  7. How do you handle API calls in React?
  8. Why should API components handle loading and error states?
  9. How do you send a POST request using fetch?

19 Key Takeaways

  • Promises represent future asynchronous results.
  • async/await provides readable Promise-based code.
  • fetch() returns a Promise.
  • Check response.ok for HTTP failures.
  • React API components commonly manage loading, error, and data states.
  • Use an inner async function when making asynchronous work from an effect.

Conclusion

React does not replace JavaScript's asynchronous model. React applications still use Promises, async/await, fetch, and normal JavaScript error handling. Understanding these fundamentals is essential for building real applications that communicate reliably with backend APIs.

Comments & Ratings (0)

Log in or create a student account to comment and rate this article.

Be the first to comment on this article.

Never miss a new post 📬

Get an email whenever a new article, project, cheatsheet or video drops.

✅

ℹ️