Модуль 7 · урок 3 з 10
Обробка помилок
«Error boundaries» у формулюванні ліда — це не React-термін, а загальна ідея: збій в одному місці не має валити весь екран, і користувач має розуміти, що сталось і що робити. Частину цього ти вже зробив у модулі 4 — тут доведемо до ладу.
Три рівні обробки
Рівень 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»: збій другорядного джерела не має ламати весь екран. Замість імені виконавця покажеться прочерк — і це набагато краще, ніж порожня сторінка з помилкою.