Модуль 4 · урок 7 з 13
Помилки в потоках
У критеріях таблиці це названо «graceful error handling». За цим стоїть проста річ:
помилка завершує потік назавжди, тому обробити її треба всередині ланцюжка, а не зовні.
Один неправильно поставлений catchError — і фільтр пошуку перестає працювати
після першого збою.
Головне правило: помилка вбиває потік
// ❌ після першої помилки цей потік мертвий назавжди
query$.pipe(
switchMap(q => this.api.search(q)),
).subscribe({
next: r => this.rows.set(r),
error: e => this.error.set('Помилка'), // спрацює один раз — і все
});
// ✅ помилка ловиться всередині — гине лише внутрішній потік, зовнішній живий
query$.pipe(
switchMap(q => this.api.search(q).pipe(
catchError(() => {
this.error.set('Не вдалось знайти');
return of([]); // підставляємо запасне значення
}),
)),
).subscribe(r => this.rows.set(r));
catchError ставлять усередині switchMap, а не після нього.catchError і що з нього повертати
catchError(err => of([])); // запасне значення — потік живе далі
catchError(err => EMPTY); // нічого не віддати, але завершити тихо
catchError(err => throwError(() => err)); // прокинути далі, обробивши по дорозі
// зазвичай хочеться і повідомити, і не зламати потік
catchError((err: HttpErrorResponse) => {
this.error.set(this.humanMessage(err));
return of([]);
})
Людські повідомлення замість кодів
private humanMessage(err: HttpErrorResponse): string {
if (err.status === 0) return 'Немає звʼязку з сервером';
if (err.status === 403) return 'Немає прав на цю дію';
if (err.status === 404) return 'Запис не знайдено';
if (err.status >= 500) return 'Помилка на сервері, спробуйте пізніше';
return 'Щось пішло не так';
}
Це та дрібниця, яку помічають на демо. Показати користувачу Http failure response
for /api/tasks: 500 Internal Server Error — означає не подумати про нього взагалі.
retry — повторити спробу
this.api.getTasks().pipe(
retry({ count: 2, delay: 1000 }), // ще дві спроби з паузою в секунду
catchError(err => { … }),
);
// зростаюча пауза: 1с, 2с, 4с — щоб не добивати сервер, який і так лежить
retry({
count: 3,
delay: (err, attempt) => timer(1000 * Math.pow(2, attempt - 1)),
})
Повторювати варто лише читання й лише мережеві помилки. Повторний
POST може створити другий запис, повторний платіж — списати гроші двічі.
І немає сенсу повторювати 403 чи 404: другого разу відповідь буде така сама.
finalize — прибрати за собою
this.loading.set(true);
this.api.getTasks().pipe(
finalize(() => this.loading.set(false)), // спрацює і при успіху, і при помилці, і при скасуванні
).subscribe(…);
Це правильне місце для вимкнення індикатора завантаження. Якщо робити це в
next, спінер зависне назавжди при першій же помилці.
Глобальний перехоплювач
// core/interceptors/error.interceptor.ts
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const toasts = inject(ToastService);
return next(req).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 0) toasts.error('Немає звʼязку з сервером');
if (err.status === 401) inject(Router).navigate(['/login']);
if (err.status >= 500) toasts.error('Помилка на сервері');
return throwError(() => err); // далі хай обробляє той, хто викликав
}),
);
};
// app.config.ts
provideHttpClient(withInterceptors([errorInterceptor]))
Розподіл праці простий: інтерсептор ловить те, що стосується всього застосунку — втрату звʼязку, неавторизованість, падіння сервера. Конкретний екран ловить те, що стосується саме його — і показує це в потрібному місці інтерфейсу.
catchErrorусерединіswitchMap, не після.- Індикатор завантаження вимикати у
finalize. - Повторювати лише читання й лише мережеві збої.
- Повідомлення людською мовою, а не текст помилки з бекенду.
- Після однієї помилки екран перестає реагувати назавжди.
- Спінер крутиться вічно після збою.
- Користувач бачить
500 Internal Server Error. retryнаPOST.