Angular IDPStrong Junior
🔥 0

Модуль 7 · урок 3 з 10

Обробка помилок

«Error boundaries» у формулюванні ліда — це не React-термін, а загальна ідея: збій в одному місці не має валити весь екран, і користувач має розуміти, що сталось і що робити. Частину цього ти вже зробив у модулі 4 — тут доведемо до ладу.

Три рівні обробки

1 · HTTP-ІНТЕРСЕПТОР немає звʼязку · 401 · 500 однаково для всього застосунку 2 · ЕКРАН що робити саме тут: показати, повторити, відкотити 3 · ERRORHANDLER усе, що ніхто не спіймав лог і повідомлення Помилка проходить рівні згори вниз. Кожен вирішує: обробити тут і зупинити, чи прокинути далі. Третій рівень існує на випадок, коли перші два не спрацювали. Без третього рівня необроблена помилка просто зникає в консолі — і про неї ніхто не дізнається.
Розподіл праці: загальне — в інтерсепторі, контекстне — на екрані, непередбачене — у глобальному обробнику.

Рівень 1: інтерсептор

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const toasts = inject(ToastService);
  const router = inject(Router);

  return next(req).pipe(
    catchError((err: HttpErrorResponse) => {
      if (err.status === 0) {
        toasts.error('Немає звʼязку з сервером');
      } else if (err.status === 401) {
        router.navigate(['/login']);
      } else if (err.status >= 500) {
        toasts.error('Помилка на сервері. Спробуйте пізніше.');
      }
      return throwError(() => err);   // далі хай вирішує екран
    }),
  );
};

Тут обробляється те, що не залежить від екрана. Головне — прокинути помилку далі: інакше екран не дізнається про збій і залишиться в стані завантаження назавжди.

Рівень 2: екран

load(): void {
  this.loading.set(true);
  this.error.set(null);

  this.api.list(this.state()).pipe(
    catchError((err: HttpErrorResponse) => {
      this.error.set(this.humanMessage(err));
      return of({ items: [], total: 0 });     // потік живий, екран показує помилку
    }),
    finalize(() => this.loading.set(false)),
  ).subscribe(page => this.page.set(page));
}

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 'Не вдалось завантажити дані.';
}

Компонент помилки з дією

@Component({
  selector: 'app-error-state',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="state" role="alert">
      <p class="state__title">{{ message() }}</p>
      <p class="state__hint">Дані могли не завантажитись через тимчасовий збій.</p>
      <button type="button" (click)="retry.emit()">Спробувати ще раз</button>
    </div>
  `,
})
export class ErrorState {
  message = input.required<string>();
  retry = output<void>();
}
Кнопка «Спробувати ще раз» обовʼязкова

Повідомлення без дії лишає людину в глухому куті: єдиний вихід — перезавантажити сторінку й утратити всі фільтри. Кнопка повтору коштує один рядок і рятує саме в тій ситуації, коли користувач і так роздратований.

Рівень 3: глобальний обробник

@Injectable()
export class AppErrorHandler implements ErrorHandler {
  private readonly toasts = inject(ToastService);

  handleError(error: unknown): void {
    console.error(error);                       // у проді — відправка в моніторинг

    // HTTP-помилки вже оброблені вище — не дублюємо повідомлення
    if (error instanceof HttpErrorResponse) return;

    this.toasts.error('Щось пішло не так. Ми вже знаємо про це.');
  }
}

// app.config.ts
providers: [{ provide: ErrorHandler, useClass: AppErrorHandler }]

Часткові збої

// ❌ один упалий довідник валить увесь екран
forkJoin({
  tasks: this.api.tasks(),
  users: this.api.users(),
  stats: this.api.stats(),
}).subscribe(…);

// ✅ кожне джерело обробляє свій збій — екран покаже те, що вдалось
forkJoin({
  tasks: this.api.tasks(),                                  // критично — без нього немає екрана
  users: this.api.users().pipe(catchError(() => of([]))),   // не критично
  stats: this.api.stats().pipe(catchError(() => of(null))), // не критично
}).subscribe(…);

Ось що насправді означає «error boundary»: збій другорядного джерела не має ламати весь екран. Замість імені виконавця покажеться прочерк — і це набагато краще, ніж порожня сторінка з помилкою.