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 попыток — это только усугубит проблему.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ