Обработка ошибок HTTP

В Angular работа с HTTP строится поверх HttpClient, который возвращает поток Observable. Любая ошибка HTTP-запроса в этой архитектуре считается ошибкой потока, а не обычным значением. Это означает, что ошибка прерывает поток данных и должна обрабатываться специальными механизмами RxJS.

HTTP-ошибки условно делятся на два типа:

  • Ошибки клиента — проблемы на стороне браузера или сети (например, отсутствие соединения).
  • Ошибки сервера — ответы с кодами 4xx и 5xx, возвращаемые сервером.

Angular объединяет оба типа в объект HttpErrorResponse.


Класс HttpErrorResponse

При возникновении ошибки HttpClient выбрасывает экземпляр HttpErrorResponse. Его ключевые свойства:

  • status — HTTP-код ответа (0 для сетевых ошибок).
  • statusText — текстовое описание статуса.
  • message — сообщение об ошибке.
  • error — тело ошибки (может быть строкой, объектом или ProgressEvent).
  • url — адрес запроса, вызвавшего ошибку.

Пример структуры:

{
  status: 404,
  statusText: "Not Found",
  message: "Http failure response for /api/users: 404 Not Found",
  error: { detail: "User not found" }
}

Обработка ошибок в подписке

Самый базовый способ — использование второго аргумента метода subscribe.

this.http.get<User[]>('/api/users').subscribe(
  data => {
    this.users = data;
  },
  error => {
    console.error(error);
  }
);

Недостатки подхода:

  • Дублирование логики обработки ошибок.
  • Отсутствие централизованного контроля.
  • Низкая масштабируемость при большом количестве запросов.

Использование операторов RxJS

Для более гибкой обработки ошибок применяется оператор catchError.

import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

this.http.get<User[]>('/api/users').pipe(
  catchError(error => {
    return throwError(() => error);
  })
);

catchError позволяет:

  • Анализировать ошибку до передачи подписчику.
  • Преобразовывать ошибку в другое значение.
  • Подменять поток альтернативным Observable.

Преобразование ошибок в пользовательские сообщения

Часто сервер возвращает технические данные, непригодные для отображения. Логика преобразования может выглядеть следующим образом:

catchError((error: HttpErrorResponse) => {
  let message = 'Неизвестная ошибка';

  if (error.error instanceof ErrorEvent) {
    message = 'Ошибка сети или клиента';
  } else {
    switch (error.status) {
      case 400:
        message = 'Некорректный запрос';
        break;
      case 401:
        message = 'Требуется авторизация';
        break;
      case 403:
        message = 'Доступ запрещён';
        break;
      case 500:
        message = 'Ошибка сервера';
        break;
    }
  }

  return throwError(() => message);
});

Централизация обработки ошибок через сервис

Создание отдельного сервиса позволяет избежать повторения кода.

@Injectable({ providedIn: 'root' })
export class HttpErrorService {

  handle(error: HttpErrorResponse): Observable<never> {
    let message = 'Ошибка запроса';

    if (error.status === 0) {
      message = 'Проблема с соединением';
    }

    return throwError(() => message);
  }
}

Использование:

this.http.get('/api/data').pipe(
  catchError(error => this.errorService.handle(error))
);

HTTP Interceptor как основной инструмент

Интерсепторы позволяют перехватывать все HTTP-запросы и ответы приложения.

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {

    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        // глобальная логика
        return throwError(() => error);
      })
    );
  }
}

Регистрация:

providers: [
  {
    provide: HTTP_INTERCEPTORS,
    useClass: ErrorInterceptor,
    multi: true
  }
]

Преимущества:

  • Единая точка обработки ошибок.
  • Минимизация логики в компонентах.
  • Возможность логирования, редиректов, уведомлений.

Обработка ошибок авторизации

Типичный сценарий — истёкший токен (401 Unauthorized).

if (error.status === 401) {
  this.authService.logout();
  this.router.navigate(['/login']);
}

Такой код обычно размещается внутри интерсептора, чтобы реагировать на ошибки независимо от конкретного запроса.


Повтор запросов при ошибке

RxJS предоставляет оператор retry и retryWhen.

this.http.get('/api/data').pipe(
  retry(3)
);

Для более сложной логики:

retryWhen(errors =>
  errors.pipe(
    delay(1000),
    take(3)
  )
);

Подход применяется для нестабильных сетевых условий или временных ошибок сервера.


Ошибки в шаблонах и асинхронных пайпах

При использовании async pipe ошибка Observable не отображается напрямую. Поток просто завершается.

<div *ngIf="data$ | async as data">
  {{ data }}
</div>

Для отображения ошибок используется обёртка потока:

data$ = this.http.get('/api/data').pipe(
  catchError(err => of({ error: true }))
);

Логирование и мониторинг

Интерсептор часто используется для отправки ошибок во внешние системы:

  • Sentry
  • LogRocket
  • Elastic Stack
catchError(error => {
  this.logger.log(error);
  return throwError(() => error);
});

Логирование должно быть отделено от отображения ошибок пользователю.


Разделение технических и бизнес-ошибок

Сервер может возвращать корректный HTTP-статус, но бизнес-ошибку в теле ответа.

{
  "success": false,
  "errorCode": "USER_BLOCKED"
}

В этом случае ошибка не считается HTTP-ошибкой и обрабатывается в map, а не catchError.

map(response => {
  if (!response.success) {
    throw new Error(response.errorCode);
  }
  return response;
});

Стратегия обработки ошибок в Angular-приложении

Типовая архитектура включает:

  • Интерсептор для глобальных ошибок.
  • Сервис для преобразования сообщений.
  • Минимальную обработку в компонентах.
  • RxJS-операторы для повторов и fallback-логики.

Такой подход обеспечивает предсказуемое поведение приложения, упрощает сопровождение и снижает связанность кода.