Skip to content

Chapter 4.4: Error Handling

Study time: 45 minutes


1. Error Handling: Mission Control Emergency Protocols

In space, anything can go wrong: a solar flare can interrupt communication, a ship's onboard computer can malfunction, and a command from Earth can contain incorrect coordinates.

Error handling on the frontend is your Mission Control's emergency protocols. They must:

  • 🚨 Prevent the entire interface from "exploding" due to a single failed command.
  • 📡 Clearly inform the operator (user) what exactly went wrong.
  • 🔧 Suggest possible next steps.

💡 Space Analogy:

If a 500 Internal Server Error signal comes from a ship, the Mission Control display should not show "Critical JavaScript error on line 57". Instead, it should be: "🚨 Onboard malfunction! Engineers have been notified. Please try the command again later."


2. Types of "Space Anomalies"

On the frontend, we encounter three main types of errors when working with an API:

  1. Network Errors: The connection to the server is not established. The antenna is not working, the cable is cut. fetch will fall into the .catch() block.
  2. Client Errors (4xx): The command from Earth was incorrect. Invalid ID, validation error. The server responds, but with a 4xx status.
  3. Server Errors (5xx): A failure on the ship itself. A problem in the API code. The server responds, but with a 500+ status.

We have already started handling them using try...catch and checking response.ok. Now let's do it centrally.


3. Centralized Handler Function

Repeating the same try...catch code in every function is bad practice. Let's create a universal "wrapper" for our fetch requests.

Step 1: Create api.js Create a new file api.js next to app.js. We will move all the API interaction logic into it.

// api.js

const API_BASE_URL = 'http://127.0.0.1:8000';

/**
 * Universal function for making API requests.
 * Handles errors and returns JSON.
 * @param {string} endpoint - The API endpoint, e.g., '/spaceships'
 * @param {object} options - Options for fetch (method, headers, body)
 */
async function apiRequest(endpoint, options = {}) {
    const url = `${API_BASE_URL}${endpoint}`;

    try {
        const response = await fetch(url, options);

        // If the response is not JSON at all, throw an error immediately
        const contentType = response.headers.get('content-type');
        if (!contentType || !contentType.includes('application/json')) {
            // Exception for a successful DELETE request, which has no body
            if (response.status === 204) return null;

            throw new TypeError(`Received a non-JSON response from the server: ${response.statusText}`);
        }

        const data = await response.json();

        if (!response.ok) {
            // If the server returned JSON with an error (e.g., detail from FastAPI)
            const errorMessage = data.detail || `HTTP error! Status: ${response.status}`;
            throw new Error(errorMessage);
        }

        return data;

    } catch (error) {
        console.error(`API request error to ${endpoint}:`, error);
        // "Re-throw" the error so it can be caught in the UI
        throw error;
    }
}

Step 2: Include api.js in index.html It is important to include it BEFORE app.js, as app.js will use its functions.

<!-- index.html -->
<body>
    <!-- ... -->
    <script src="api.js"></script>
    <script src="app.js"></script>
</body>

Step 3: Refactor app.js Now let's rewrite our functions using the new apiRequest.

// app.js

// const API_BASE_URL = ...; // This line can be removed, it is now in api.js

// ...

async function fetchAndDisplayFleet() {
    try {
        fleetList.innerHTML = '<li>Loading telemetry...</li>';
        const ships = await apiRequest('/spaceships'); // <-- Use our wrapper!

        fleetList.innerHTML = '';
        if (ships.length === 0) {
            fleetList.innerHTML = '<li>No crafts found in the registry.</li>';
            return;
        }

        ships.forEach(ship => { /* ... rest of the display code ... */ });
    } catch (error) {
        fleetList.innerHTML = `<li>🔴 Error loading fleet: ${error.message}</li>`;
    }
}

async function createShip(event) {
    event.preventDefault();
    const shipData = { /* ... collecting data from the form ... */ };

    try {
        createStatusMessage.textContent = 'Sending launch command...';
        const options = {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(shipData)
        };
        const newShip = await apiRequest('/spaceships', options); // <-- Use our wrapper!

        createStatusMessage.textContent = `🚀 Successful launch! Craft assigned ID: ${newShip.id}`;
        createShipForm.reset();
        fetchAndDisplayFleet();
    } catch (error) {
        createStatusMessage.textContent = `🔴 Error: ${error.message}`;
    }
}

// Rewrite the other functions (fetchShipById, deleteShip) similarly!
Now all the logic for handling network errors, checking response.ok, and parsing JSON is in one place, and the code in app.js has become much cleaner and more readable.


4. Displaying Errors to the User

A good interface should not just write an error to the console, but show it to the user in an understandable way.

Example: Improving createShip Our code already does this: createStatusMessage.textContent = .... But we can do even better by creating a universal function for displaying notifications.

Add to app.js:

// app.js
function showNotification(message, isError = false) {
    const notificationArea = document.getElementById('create-status-message'); // or another element
    notificationArea.textContent = message;
    notificationArea.style.color = isError ? 'red' : 'green';
}

// Use in createShip:
async function createShip(event) {
    // ...
    try {
        // ...
        const newShip = await apiRequest('/spaceships', options);
        showNotification(`🚀 Successful launch! ID: ${newShip.id}`);
        // ...
    } catch (error) {
        showNotification(`🔴 Error: ${error.message}`, true);
    }
}
Now we have a single mechanism for showing both success messages and errors.


Quiz to Reinforce

1. The `.catch()` block in a `fetch` promise will trigger if...

2. Why is a centralized handler function for API requests needed?

3. `response.headers.get('content-type')` is used to...

4. `throw new Error(...)` inside a `try...catch` or `.then()` is used to...

5. Why is it important to show errors to the user, and not just in the console?


🚀 Chapter Summary:

You have strengthened your Mission Control by creating reliable emergency protocols.

  • 🛡️ You understand the difference between network, client, and server errors.
  • ⚙️ You have created a centralized apiRequest function to handle all requests, avoiding code duplication.
  • 📡 Your interface can now correctly inform the user about errors, making it more user-friendly and reliable.

Emergency shields are up! But what is better: .then() chains or modern async/await? In the next chapter, we will look at both approaches and understand when to use which.

📌 Verification:

  • Check that your code in app.js has been successfully refactored and uses the new apiRequest function.
  • Try stopping the FastAPI server and clicking the "Request Data" button. You should see a connection error on the page.
  • Try to create a ship with invalid data. You should see the validation error message that came from FastAPI.

⚠️ If there are errors:

  • apiRequest is not defined: Make sure you have included api.js in index.html before app.js.
  • Check the browser console for other JavaScript syntax errors.