Lesson 48 of 60 – Loading and Error Handling
80%

Loading and Error Handling in React

When a React application communicates with an API, the request may take some time to complete or may fail. A good application should clearly handle loading, success, and error states.

Note: Loading and error states help users understand what the application is doing while asynchronous operations such as API requests are running.

1. What is Loading State?

A loading state tells the application that some asynchronous operation is currently in progress.

For example, while student data is being fetched from an API, you can show:

Loading students...

This prevents the user from thinking that the application is not responding.

2. Creating a Loading State

The useState Hook can be used to store the loading status.

const [loading, setLoading] = useState(true);

Here, true means that the operation is initially considered to be loading.

3. Display a Loading Message

You can conditionally display a loading message.

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

When loading is true, the component displays the loading message.

4. Turning Loading Off

After the asynchronous operation finishes, the loading state can be changed to false.

setLoading(false);

This allows the application to display the actual content.

5. What is Error State?

An error state stores information when an operation fails.

For example, an API request can fail because of:

  • Network problems
  • Server errors
  • Invalid URLs
  • Authentication problems
  • Invalid request data

6. Creating an Error State

You can create an error state with useState.

const [error, setError] = useState("");

An empty string means that there is currently no error message.

7. Handling Errors with fetch()

The catch() method can be used to handle a rejected Promise.

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

8. Saving the Error Message

The error message can be stored in React state.

.catch(error => {
    setError(error.message);
});

The component can then use the error state to display an appropriate message.

9. Handling HTTP Errors

With fetch(), HTTP error responses do not automatically reject the Promise. You can check response.ok.

fetch("/api/students")
    .then(response => {

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

        return response.json();
    });

10. Displaying an Error

You can conditionally display an error message.

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

This gives the user information when something goes wrong.

11. Loading with Axios

The same loading state pattern can be used with Axios.

const [loading, setLoading] = useState(true);

axios.get("/api/students")
    .then(response => {
        setStudents(response.data);
    })
    .finally(() => {
        setLoading(false);
    });

12. Error Handling with Axios

Axios requests can handle errors using catch().

axios.get("/api/students")
    .then(response => {
        setStudents(response.data);
    })
    .catch(error => {
        setError(error.message);
    });

13. finally() Method

The finally() method runs after a Promise settles, whether it was fulfilled or rejected.

axios.get("/api/students")
    .then(response => {
        setStudents(response.data);
    })
    .catch(error => {
        setError(error.message);
    })
    .finally(() => {
        setLoading(false);
    });

14. Three Common UI States

An API-based component commonly has three important states:

  • Loading: The request is in progress.
  • Success: The data has been received.
  • Error: The request failed.
Loading
   ↓
Request
   ↓
Success

or

Loading
   ↓
Request
   ↓
Error

15. Conditional Rendering

React conditional rendering can display different content based on the current state.

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

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

return <StudentList />;

16. Loading Before Data

A component may start with an empty data array.

const [students, setStudents] = useState([]);
const [loading, setLoading] = useState(true);

The loading message can be shown until the API request completes.

17. Handling Empty Data

An API request can succeed but return an empty array. This is different from an error.

if (students.length === 0) {
    return <p>No students found.</p>;
}

The application can provide a useful message instead of displaying a blank screen.

18. Retry After an Error

A retry button can call the same API request again after a failure.

function retryRequest() {

    setError("");

    loadStudents();
}

Retry functionality can be useful when a temporary network problem occurs.

19. Async/Await Error Handling

The try...catch statement can handle errors when using async/await.

async function loadStudents() {

    try {

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

        const data = await response.json();

        setStudents(data);

    } catch (error) {

        setError(error.message);

    }
}

20. Using finally with async/await

The finally block can be used to stop the loading state after the request finishes.

async function loadStudents() {

    try {

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

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

        const data = await response.json();

        setStudents(data);

    } catch (error) {

        setError(error.message);

    } finally {

        setLoading(false);

    }
}

21. API Loading Spinner

Instead of plain text, you can display a Bootstrap spinner while data is loading.

{loading && (
    <div className="spinner-border">
        </div>
)}

A spinner provides a visual indication that the application is working.

22. Error Alert with Bootstrap

Bootstrap alert classes can be used to display errors.

{error && (
    <div className="alert alert-danger">
        {error}
    </div>
)}

This creates a clear visual error message.

23. Loading and Error with API Data

const [students, setStudents] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");

useEffect(() => {

    fetch("/api/students")
        .then(response => {

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

            return response.json();
        })
        .then(data => {
            setStudents(data);
        })
        .catch(error => {
            setError(error.message);
        })
        .finally(() => {
            setLoading(false);
        });

}, []);

24. Complete Loading UI

if (loading) {

    return (
        <div className="text-center">

            <div
                className="spinner-border"
            >
            </div>

            <p>
                Loading students...
            </p>

        </div>
    );
}

25. Complete Error UI

if (error) {

    return (
        <div className="alert alert-danger">

            <h4>
                Something went wrong
            </h4>

            <p>
                {error}
            </p>

        </div>
    );
}

26. Avoiding Unnecessary Requests

The dependency array of useEffect should be considered carefully when making API requests.

useEffect(() => {

    loadStudents();

}, []);

For an effect that should run once after the initial render, an empty dependency array is commonly used.

27. Loading, Error, and Success Flow

Start
  ↓
loading = true
  ↓
API Request
  ↓
┌───────────────┐
│               │
Success       Error
│               │
↓               ↓
Data          Error
│               │
└───────┬───────┘
        ↓
loading = false
        ↓
Display UI

28. Common Loading and Error Handling Mistakes

  • Not showing any loading feedback.
  • Not handling API errors.
  • Forgetting to stop the loading state.
  • Trying to display data before it exists.
  • Ignoring empty API responses.
  • Not checking HTTP response status when using fetch.
  • Making unnecessary repeated API requests.
  • Showing technical error details that are not useful to users.

29. Complete Example

import {
    useEffect,
    useState
} from "react";

function Students() {

    const [students, setStudents] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState("");

    useEffect(() => {

        async function loadStudents() {

            try {

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

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

                const data = await response.json();

                setStudents(data);

            } catch (err) {

                setError(err.message);

            } finally {

                setLoading(false);

            }
        }

        loadStudents();

    }, []);

    if (loading) {

        return (
            <div className="text-center">

                <div
                    className="spinner-border"
                >
                </div>

                <p>
                    Loading students...
                </p>

            </div>
        );
    }

    if (error) {

        return (
            <div className="alert alert-danger">

                <h4>
                    Error
                </h4>

                <p>
                    {error}
                </p>

            </div>
        );
    }

    if (students.length === 0) {

        return (
            <p>
                No students found.
            </p>
        );
    }

    return (
        <div>

            <h2>
                Student List
            </h2>

            {students.map(student => (

                <div
                    key={student.id}
                    className="card mt-3"
                >

                    <div className="card-body">

                        <h4>
                            {student.name}
                        </h4>

                        <p>
                            Course:
                            {student.course}
                        </p>

                    </div>

                </div>

            ))}

        </div>
    );
}

export default Students;

30. Best Practices for Loading and Error Handling

  • Always provide useful feedback while important data is loading.
  • Handle expected API errors gracefully.
  • Use separate loading and error states when appropriate.
  • Check API responses before using the returned data.
  • Handle empty results separately from request failures.
  • Use try...catch...finally with async/await when suitable.
  • Keep error messages understandable to users.
  • Avoid unnecessary API requests.
  • Keep API and UI state management organized.
  • Provide retry functionality when it makes sense for the application.
Loading
   ↓
Request
   ↓
Success → Display Data

or

Loading
   ↓
Request
   ↓
Error → Display Error

📌 Key Points

  • Loading state indicates that an asynchronous operation is in progress.
  • Error state stores information about a failed operation.
  • useState can manage loading, error, and data states.
  • fetch() can use catch() for rejected Promises.
  • With fetch, response.ok can be checked for HTTP errors.
  • Axios provides catch() and finally() for request handling.
  • Loading, success, and error states should be considered when building API interfaces.
  • Empty API results should be handled separately from errors.
  • Bootstrap spinners and alerts can improve the user experience.
  • Good error handling makes React applications easier to use and maintain.

🧠 Quick Quiz

Question: Which state is commonly used to indicate that an API request is currently in progress?