JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Обработка ошибок HTTP-запросов:

Обработка ошибок HTTP-запросов: catchError в Angular

Модуль 4: Node.js, Next.js и Angular
17 уровень , 4 лекция
Открыта

1. Введение

Давайте честно: если вы делаете приложение, которое "падает" при первом же сетевом сбое — пользователи быстро найдут себе другое. Ошибки HTTP — это не только 500 Internal Server Error, но и 404 Not Found, 401 Unauthorized, 0 (нет соединения), да и просто тайм-ауты.

Если не обработать ошибку, Angular выбросит её в консоль, а ваш компонент может остаться в "подвешенном состоянии" — например, крутится спиннер загрузки и не исчезает никогда. Не лучший пользовательский опыт, правда?

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

Как ошибки попадают в Observable?

Когда вы делаете HTTP-запрос с помощью Angular HttpClient, результат — это Observable. Если сервер вернёт ошибку (например, 404 или 500), Observable не выдаст "значение ошибки", а завершится с ошибкой, то есть вызовет свой метод error. Это как в промисах: либо вы получаете resolve, либо reject.

Пример:

this.http.get('/api/data').subscribe({
  next: data => console.log('Данные:', data),
  error: err => console.error('Ошибка!', err)
});

Если сервер ответит ошибкой, будет вызван блок error. Но такой подход неудобен: все ошибки приходится обрабатывать прямо в компоненте, а хочется — централизованно и красиво!

RxJS-оператор catchError: что это и зачем нужен

catchError — это оператор RxJS, который позволяет "поймать" ошибку внутри Observable-цепочки и превратить её в какое-то другое действие. Например, вернуть запасной результат, повторить запрос, или пробросить ошибку дальше.

Синтаксис:

import { catchError } from 'rxjs/operators';

this.http.get('/api/data').pipe(
  catchError(error => {
    // обработка ошибки
    return of([]); // вернуть пустой массив вместо ошибки
  })
).subscribe(data => {
  // здесь уже не будет ошибки, даже если сервер упал
});

В отличие от обработки в subscribe, catchError позволяет перехватить ошибку на уровне сервиса или data-layer, не засоряя компоненты.

2. Практический пример: базовая обработка ошибки

Допустим, у нас есть сервис, который загружает список пользователей:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class UserService {
  constructor(private http: HttpClient) {}

  getUsers() {
    return this.http.get<User[]>('/api/users').pipe(
      catchError(error => {
        console.error('Ошибка при загрузке пользователей:', error);
        return of([]); // Возвращаем пустой массив вместо ошибки
      })
    );
  }
}

Теперь в компоненте можно просто подписаться:

this.userService.getUsers().subscribe(users => {
  this.users = users;
  // Даже если произошла ошибка, users будет []
});

Пользователь не увидит "красную ошибку" — просто не будет данных.

3. Различие между catchError и обработчиком error в subscribe

Многие новички путаются: зачем нужен catchError, если можно ловить ошибку в subscribe? Разница в том, что catchError позволяет обработать ошибку до того, как Observable завершится с ошибкой. Вы можете вернуть запасное значение, повторить запрос, или пробросить ошибку дальше.

Если не использовать catchError:

this.http.get('/api/data').subscribe({
  next: data => { ... },
  error: err => { ... } // ошибка попадает сюда
});

Если использовать catchError:

this.http.get('/api/data').pipe(
  catchError(err => {
    // Здесь можно обработать ошибку и вернуть запасной Observable
    return of('default value');
  })
).subscribe(data => {
    // Ошибка уже не попадёт сюда, вместо неё придёт 'default value'
});

Вывод:
catchError — для обработки ошибок внутри потока.
error в subscribe — для финальной обработки, если ошибки не были перехвачены ранее.

4. Пример: отображение сообщения об ошибке

Иногда хочется не просто вернуть запасное значение, а показать пользователю ошибку. Для этого можно использовать Subject или свойство компонента.

import { BehaviorSubject, of } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Component({ ... })
export class UserListComponent {
  errorMessage = '';
  users: User[] = [];

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.userService.getUsers().pipe(
      catchError(err => {
        this.errorMessage = 'Не удалось загрузить пользователей!';
        // Можно залогировать ошибку
        return of([]); // Возвращаем пустой массив
      })
    ).subscribe(users => {
      this.users = users;
    });
  }
}

В шаблоне:

<div *ngIf="errorMessage" class="alert alert-danger">
  {{ errorMessage }}
</div>
<ul>
  <li *ngFor="let user of users">{{ user.name }}</li>
</ul>

Теперь если что-то пошло не так, пользователь увидит красивое сообщение, а не "Cannot read property 'name' of undefined".

5. Пример: обработка разных типов ошибок

Иногда нужно по-разному реагировать на разные ошибки: например, если 401 — отправить на логин, если 404 — показать "не найдено", если 500 — "сервер сломался".

Angular HttpClient возвращает ошибку типа HttpErrorResponse, у которой есть полезные поля: status, statusText, error.

import { HttpErrorResponse } from '@angular/common/http';

catchError((error: HttpErrorResponse) => {
  if (error.status === 404) {
    this.errorMessage = 'Пользователи не найдены!';
  } else if (error.status === 401) {
    this.errorMessage = 'Войдите в систему!';
    // Можно сделать редирект на страницу логина
  } else if (error.status === 0) {
    this.errorMessage = 'Нет соединения с сервером!';
  } else {
    this.errorMessage = 'Что-то пошло не так: ' + error.message;
  }
  return of([]);
})

6. Пример: пробрасывание ошибки дальше (rethrow)

Иногда вы хотите обработать ошибку на уровне сервиса, но не глотать её, а пробросить дальше, чтобы компонент мог что-то сделать.

Для этого используйте оператор throwError:

import { throwError } from 'rxjs';

getUsers() {
  return this.http.get<User[]>('/api/users').pipe(
    catchError(error => {
      // Логируем ошибку, но пробрасываем дальше
      console.error('Ошибка:', error);
      return throwError(() => error);
    })
  );
}

// В компоненте:
this.userService.getUsers().subscribe({
  next: users => { ... },
  error: err => {
    // Здесь обработаем ошибку, если нужно
    this.errorMessage = 'Ошибка загрузки пользователей!';
  }
});

7. Пример: повтор запроса при ошибке (retry)

Иногда сеть "плавает" и достаточно просто повторить запрос. Для этого есть оператор retry:

import { retry, catchError } from 'rxjs/operators';

this.http.get('/api/data').pipe(
  retry(3), // Попробовать до 3 раз
  catchError(err => {
    this.errorMessage = 'Сервер не отвечает после 3 попыток';
    return of([]);
  })
).subscribe(...);

8. Централизованная обработка ошибок: HTTP Interceptor

Если вы хотите обрабатывать ошибки всех HTTP-запросов в одном месте (например, для логирования или автоматического выхода при 401), используйте HTTP Interceptor.

Пример:

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        // Глобальная обработка ошибок
        if (error.status === 401) {
          // Например, разлогинить пользователя
        }
        // Можно показывать тосты, отправлять логи и т.д.
        return throwError(() => error);
      })
    );
  }
}

Не забудьте зарегистрировать интерцептор в модуле.

9. Типичные ошибки при обработке ошибок HTTP-запросов

Ошибка №1: "Глотаем" ошибку и забываем про пользователя.
Если в catchError вы просто возвращаете запасное значение, пользователь может не понять, что что-то пошло не так. Всегда информируйте пользователя о проблеме!

Ошибка №2: Не возвращаем Observable из catchError.
Если забыть вернуть Observable (of(...) или throwError(...)), цепочка RxJS "сломается", и вы получите ошибку типа "You provided 'undefined' where a stream was expected".

Ошибка №3: Обработка ошибок только в subscribe.
Это приводит к дублированию кода в каждом компоненте. Лучше обрабатывать ошибки на уровне сервиса или через интерцептор.

Ошибка №4: Не различаем типы ошибок.
Все ошибки — не одинаковы! 404, 401, 500 и просто отсутствие соединения требуют разной реакции.

Ошибка №5: Перебор с retry.
Если сервер действительно "лежит", не стоит делать 100 попыток — это только усугубит проблему.

3
Опрос
Подключение HttpClientModule, 17 уровень, 4 лекция
Недоступен
Подключение HttpClientModule
Подключение HttpClientModule
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ