サンプルコードで身につける: HTTPエラーへの備え — catchError と status
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1catchError — エラー時に代替値で流れを続ける
通信に失敗したとき、catchErrorでof([])を返して「空の一覧が届いた」ことにする最小例です。エラーで流れを止めずに画面を守る、防御の基本形です。ofは渡した値をすぐ発行するObservableを作る関数で、catchErrorの返り値はObservableである必要があります。
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe } from '@angular/common';
import { catchError, of } from 'rxjs';
type News = { id: number; title: string };
@Component({
selector: 'app-news-list',
imports: [AsyncPipe],
template:
'@for (n of news$ | async; track n.id) { <p>{{ n.title }}</p> }' +
' @empty { <p>ニュースを取得できませんでした</p> }',
})
export class NewsListComponent {
private http = inject(HttpClient);
news$ = this.http.get<News[]>('/api/news').pipe(
// 失敗しても空配列を流して画面を守る
catchError(() => of([] as News[]))
);
}
2HttpErrorResponseのstatusで原因を見分ける
catchErrorで受け取るHttpErrorResponseのstatusを見て、0(ネットワーク断)・404(見つからない)・500(サーバーエラー)を出し分ける例です。原因ごとにユーザーへ伝えるべき言葉は違う、という実務の感覚をコードにしています。statusが0のときはサーバーに到達すらしていない点が重要です。
import { Component, inject, signal } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
type Report = { id: number; title: string };
@Component({
selector: 'app-report',
template: '<p>{{ message() }}</p>',
})
export class ReportComponent {
private http = inject(HttpClient);
message = signal('読み込み中...');
ngOnInit() {
this.http.get<Report[]>('/api/reports').subscribe({
next: (list) => this.message.set(list.length + ' 件のレポート'),
error: (err: HttpErrorResponse) => {
// status で原因を見分けて言葉を変える
if (err.status === 0) {
this.message.set('ネットワークに接続できません');
} else if (err.status === 404) {
this.message.set('レポートが見つかりません');
} else if (err.status >= 500) {
this.message.set('サーバーで問題が発生しています');
} else {
this.message.set('エラーが発生しました(' + err.status + ')');
}
},
});
}
}
3エラー通知をシグナルで画面に出す
catchErrorの中でエラーメッセージのシグナルを更新し、画面上部のバナーとして表示する例です。console.logだけではユーザーには何も伝わらないため、「起きたことを画面で知らせる」ことをエラー処理のゴールにします。代替値も返すので一覧部分は空のまま安全に描画されます。
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
import { catchError, of } from 'rxjs';
type Photo = { id: number; url: string };
@Component({
selector: 'app-gallery',
template:
'@if (errorMessage()) {' +
' <p class="banner">{{ errorMessage() }}</p>' +
'}' +
'@for (p of photos(); track p.id) { <img [src]="p.url"> }',
})
export class GalleryComponent {
private http = inject(HttpClient);
errorMessage = signal('');
photos = toSignal(
this.http.get<Photo[]>('/api/photos').pipe(
catchError(() => {
// ユーザーに見える場所へ通知し、空の一覧で続行する
this.errorMessage.set('写真の取得に失敗しました。時間をおいて再読み込みしてください');
return of([] as Photo[]);
})
),
{ initialValue: [] as Photo[] }
);
}
4retry — 一時的な失敗は自動でやり直す
catchErrorの前にretry(2)を置き、失敗したら最大2回まで自動で再送する例です。モバイル回線の瞬断など一時的なエラーはリトライで回復することが多く、それでもだめならcatchErrorが引き取ります。「まず再試行、最後に代替値」という二段構えは読み取り系APIの定番です。
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe } from '@angular/common';
import { catchError, of, retry } from 'rxjs';
type Weather = { city: string; temp: number };
@Component({
selector: 'app-weather',
imports: [AsyncPipe],
template:
'@if (weather$ | async; as w) {' +
' <p>{{ w.city }}: {{ w.temp }} ℃</p>' +
'}',
})
export class WeatherComponent {
private http = inject(HttpClient);
weather$ = this.http.get<Weather>('/api/weather').pipe(
retry(2), // 一時的な失敗は最大2回までやり直す
// 2回やり直してもだめなら代替値に切り替える
catchError(() => of({ city: '取得失敗', temp: 0 }))
);
}
5サービス層でエラー処理をまとめる
エラー処理をサービスの中に閉じ込め、コンポーネントは「必ず配列が届く」前提で書けるようにした構成例です。同じAPIを複数の画面から使う場合でも、備えを1か所に集約できます。エラーの詳細はconsole.errorで開発者向けに残しつつ、呼び出し側には安全な値だけを返します。
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, catchError, of } from 'rxjs';
export type Product = { id: number; name: string };
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient);
// 呼び出し側は「必ず Product[] が届く」前提で書ける
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products').pipe(
catchError((err: HttpErrorResponse) => {
// 開発者向けの記録はここで済ませる
console.error('商品一覧の取得に失敗:', err.status, err.message);
return of([] as Product[]);
})
);
}
}
// コンポーネント側はエラー処理を意識せずに使える:
// products = toSignal(inject(ProductService).getProducts(),
// { initialValue: [] as Product[] });