エラーハンドリングについて考えてみた


はじめに

今回は、外部APIからデータを取得して表示する際のエラーハンドリングについて考えてみます。

方法1

ERROR HANDLING WITH ANGULAR`S ASYNC PIPE でも紹介されている方法です。
エラーを通知する先としてSubjectを用意してそれをビューテンプレートで表示します。

Service

テスト用のモックを作成します。(実際に外部APIと通信は行いません。)

get(id: number): Observable<User> {
  return new Observable((subscriber) => {
    setTimeout(() => {
      if (!id) {
        subscriber.error('id is required');
        return
      }
      subscriber.next(new User(id, "hello"))
      subscriber.complete();
    }, 1000);
  })
}

Component

user$: Observable<User> | null = null;
error$ = new Subject<Error>();

get(id: number) {
    this.error$.next()
    this.user$ = this.userService.get(id).pipe(
      catchError(err => {
        this.error$.next(err);
        return throwError(err);
      })
    );
}

View

asyncでuserオブジェクトを取得できない場合は、ローディング中もしくは、エラーが発生している可能性があります。
その場合は loadingOrErrorTemp を表示して、その中で、エラーが出ている場合とローディング中の場合を、error$の値を参照して判定します。

<ng-container *ngIf="user$">
    <div *ngIf="user$ | async as user; else loadingOrErrorTemp">{{user | json}}</div>
</ng-container>
<ng-template #loadingOrErrorTemp>
    <div *ngIf="error$ | async as err; else loadingTemp">{{err}}</div>
    <ng-template #loadingTemp>loading</ng-template>
</ng-template>

方法2

次の方法は、Go言語のエラーハンドリングからヒントを得た方法です。
Goでは、以下のように、メソッドの戻り値として、エラーを受け取りハンドリングを行います。

value, err := get(id)
if err != nil {
    log.Println(err)
    return
}

これをAngularのエラーハンドリングに適応すると以下のようになります。

Object

userオブジェクトをラップして、エラーの値もサービスから返すようにします。

export class UserResponse {
    user?: User;
    errorMsg?: string;
}

Service

userResponseオブジェクトを返すサービスを作成します。

getUserResponse(id: number): Observable<UserResponse> {
  return new Observable((subscriber) => {
    setTimeout(() => {
      const userResponse = new UserResponse();

      if (!id) {
        const errMsg = 'id is required';
        userResponse.errorMsg = errMsg
        subscriber.next(userResponse);
        subscriber.complete();
        return
      }

      userResponse.user = new User(id, "hello");
      subscriber.next(userResponse);
      subscriber.complete();
    }, 1000);
  })
}

Component

getUserResponse(id: number) :Observable<UserResponse>{
    this.userResponse$ = this.userService.getUserResponse(id);
}

Template

前回の方法と異なり、asyncで値を取得できていない状態はローディングの時のみとなります。

<ng-container *ngIf="userResponse$">
    <div *ngIf="userResponse$ | async as userResponse; else loadingTemp">
        <ng-container *ngIf="userResponse.errorMsg as err">{{err}}</ng-container>
        <ng-container *ngIf="userResponse.user as user">{{user?.id}}</ng-container>
    </div>
    <ng-template #loadingTemp>loading</ng-template>
</ng-container>

まとめ

どちらの方法が良いかは好みかと思いますが、個人的には方法2の方が直感的でコードの見通しも良いのではと思いました。