Skip to content

Глава 4.4: Обработка ошибок

Время изучения: 45 минут


1. Обработка ошибок: Аварийные протоколы ЦУП

В космосе все может пойти не по плану: солнечная вспышка может прервать связь, в бортовом компьютере корабля может произойти сбой, а команда с Земли может содержать неверные координаты.

Обработка ошибок на фронтенде — это аварийные протоколы вашего ЦУПа. Они должны:

  • 🚨 Не дать всему интерфейсу "взорваться" из-за одной неудачной команды.
  • 📡 Четко сообщить оператору (пользователю), что именно пошло не так.
  • 🔧 Предложить возможные дальнейшие действия.

💡 Космическая аналогия:

Если от корабля приходит сигнал 500 Internal Server Error, на дисплее ЦУПа не должно появляться "Критическая ошибка JavaScript в строке 57". Вместо этого должно быть: "🚨 Сбой на борту корабля! Инженеры уже уведомлены. Попробуйте повторить команду позже."


2. Типы "космических аномалий"

На фронтенде мы сталкиваемся с тремя основными типами ошибок при работе с API:

  1. Сетевые ошибки: Связь с сервером не установлена. Антенна не работает, кабель перерезан. fetch "упадет" в блок .catch().
  2. Клиентские ошибки (4xx): Команда с Земли была некорректной. Неверный ID, ошибка валидации. Сервер отвечает, но со статусом 4xx.
  3. Серверные ошибки (5xx): Сбой на самом корабле. Проблема в коде API. Сервер отвечает, но со статусом 500+.

Мы уже начали обрабатывать их с помощью try...catch и проверки response.ok. Теперь давайте сделаем это централизованно.


3. Централизованная функция-обработчик

Повторять один и тот же код try...catch в каждой функции — плохая практика. Создадим универсальную "обертку" для наших fetch-запросов.

Шаг 1: Создаем api.js Создайте новый файл api.js рядом с app.js. В него мы вынесем всю логику взаимодействия с API.

// api.js

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

/**
 * Универсальная функция для выполнения запросов к API.
 * Обрабатывает ошибки и возвращает JSON.
 * @param {string} endpoint - Конечная точка API, например, '/spaceships'
 * @param {object} options - Параметры для fetch (method, headers, body)
 */
async function apiRequest(endpoint, options = {}) {
    const url = `${API_BASE_URL}${endpoint}`;

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

        // Если ответ вообще не JSON, то сразу выбрасываем ошибку
        const contentType = response.headers.get('content-type');
        if (!contentType || !contentType.includes('application/json')) {
            // Исключение для успешного DELETE запроса, который не имеет тела
            if (response.status === 204) return null;

            throw new TypeError(`Получен не-JSON ответ от сервера: ${response.statusText}`);
        }

        const data = await response.json();

        if (!response.ok) {
            // Если сервер вернул JSON с ошибкой (например, detail от FastAPI)
            const errorMessage = data.detail || `HTTP ошибка! Статус: ${response.status}`;
            throw new Error(errorMessage);
        }

        return data;

    } catch (error) {
        console.error(`Ошибка API запроса к ${endpoint}:`, error);
        // "Пробрасываем" ошибку дальше, чтобы ее можно было поймать в UI
        throw error;
    }
}

Шаг 2: Подключаем api.js в index.html Важно подключить его ДО app.js, так как app.js будет использовать его функции.

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

Шаг 3: Рефакторим app.js Теперь перепишем наши функции, используя новый apiRequest.

// app.js

// const API_BASE_URL = ...; // Эту строку можно удалить, она теперь в api.js

// ...

async function fetchAndDisplayFleet() {
    try {
        fleetList.innerHTML = '<li>Загрузка телеметрии...</li>';
        const ships = await apiRequest('/spaceships'); // <-- Используем нашу обертку!

        fleetList.innerHTML = '';
        if (ships.length === 0) {
            fleetList.innerHTML = '<li>В реестре нет ни одного аппарата.</li>';
            return;
        }

        ships.forEach(ship => { /* ... остальной код отображения ... */ });
    } catch (error) {
        fleetList.innerHTML = `<li>🔴 Ошибка загрузки флота: ${error.message}</li>`;
    }
}

async function createShip(event) {
    event.preventDefault();
    const shipData = { /* ... сбор данных из формы ... */ };

    try {
        createStatusMessage.textContent = 'Отправка команды на запуск...';
        const options = {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(shipData)
        };
        const newShip = await apiRequest('/spaceships', options); // <-- Используем нашу обертку!

        createStatusMessage.textContent = `🚀 Успешный запуск! Аппарату присвоен ID: ${newShip.id}`;
        createShipForm.reset();
        fetchAndDisplayFleet();
    } catch (error) {
        createStatusMessage.textContent = `🔴 Ошибка: ${error.message}`;
    }
}

// Перепишите остальные функции (fetchShipById, deleteShip) по аналогии!
Теперь вся логика обработки сетевых ошибок, проверки response.ok и парсинга JSON находится в одном месте, а код в app.js стал намного чище и читаемее.


4. Отображение ошибок пользователю

Хороший интерфейс должен не просто писать ошибку в консоль, а показывать ее пользователю в понятном виде.

Пример: Улучшение createShip Наш код уже делает это: createStatusMessage.textContent = .... Но можно сделать еще лучше, создав универсальную функцию для отображения уведомлений.

Добавляем в app.js:

// app.js
function showNotification(message, isError = false) {
    const notificationArea = document.getElementById('create-status-message'); // или другой элемент
    notificationArea.textContent = message;
    notificationArea.style.color = isError ? 'red' : 'green';
}

// Используем в createShip:
async function createShip(event) {
    // ...
    try {
        // ...
        const newShip = await apiRequest('/spaceships', options);
        showNotification(`🚀 Успешный запуск! ID: ${newShip.id}`);
        // ...
    } catch (error) {
        showNotification(`🔴 Ошибка: ${error.message}`, true);
    }
}
Теперь у нас есть единый механизм для показа как успешных сообщений, так и ошибок.


Квиз для закрепления

1. Блок `.catch()` в промисе `fetch` сработает, если...

2. Зачем нужна централизованная функция-обработчик для API запросов?

3. `response.headers.get('content-type')` используется для...

4. `throw new Error(...)` внутри `try...catch` или `.then()` используется, чтобы...

5. Почему важно показывать ошибки пользователю, а не только в консоли?


🚀 Итог главы:

Вы укрепили свой ЦУП, создав надежные аварийные протоколы.

  • 🛡️ Вы понимаете разницу между сетевыми, клиентскими и серверными ошибками.
  • ⚙️ Вы создали централизованную функцию apiRequest для обработки всех запросов, избежав дублирования кода.
  • 📡 Ваш интерфейс теперь умеет корректно сообщать пользователю об ошибках, делая его более дружелюбным и надежным.

Аварийные щиты подняты! Но что лучше: цепочки .then() или современный async/await? В следующей главе мы разберем оба подхода и поймем, когда какой использовать.

📌 Проверка:

  • Проверьте, что ваш код в app.js успешно рефакторен и использует новую функцию apiRequest.
  • Попробуйте остановить FastAPI сервер и нажать кнопку "Запросить данные". Вы должны увидеть ошибку соединения на странице.
  • Попробуйте создать корабль с невалидными данными. Вы должны увидеть сообщение об ошибке валидации, которое пришло от FastAPI.

⚠️ Если ошибки:

  • apiRequest is not defined: Убедитесь, что вы подключили api.js в index.html перед app.js.
  • Проверьте консоль браузера на наличие других синтаксических ошибок в JavaScript.