Dev Study
← 解説「HTTPエラーへの備え — catchError と status」に戻る

サンプルコードで身につける: 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[] });