Angular IDPStrong Junior
Модулі / Дані, моки й RxJS / Помилки в потоках
🔥 0

Модуль 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 ЗОВНІ помилка далі потік мертвий: набір у пошуку більше нічого не робить CATCHERROR УСЕРЕДИНІ SWITCHMAP of([]) зовнішній потік живий — пошук працює далі
Помилка внутрішнього потоку не має підніматись у зовнішній. Тому 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.

≈ 40 хв · +25 XP за урок