エラー処理 | 株式会社Altus-Five / 株式会社Altus-Five は、技術力で勝負するシステム開発会社です。 Sun, 01 Jun 2025 14:56:37 +0000 ja hourly 1 https://wordpress.org/?v=6.8.2 /wp-content/uploads/2025/01/cropped-favicon-32x32.png エラー処理 | 株式会社Altus-Five / 32 32 Angularのエラー処理について考える(実装編) /blog/2019/04/10/angular-error-hadling-implement/ /blog/2019/04/10/angular-error-hadling-implement/#respond Wed, 10 Apr 2019 09:31:00 +0000 http://43.207.2.176/?p=298 前回の記事では、Angularのエラー処理について設計時の目線で整理しました。今回は、それを実装するときに、どんなことになるのか、実例を添えて説明します。 エラー分類と画面側の実装 前回の設計編に記述したエラー分類別に、 […]

The post Angularのエラー処理について考える(実装編) first appeared on 株式会社Altus-Five.

]]>
前回の記事では、Angularのエラー処理について設計時の目線で整理しました。
今回は、それを実装するときに、どんなことになるのか、実例を添えて説明します。

エラー分類と画面側の実装

前回の設計編に記述したエラー分類別に、画面側に実装するエラー処理について、一覧化しました。
画面固有の機能として実装すべきものか、あるいは、共通処理として実装すべきかについて、区別します。

#エラー画面側の実装
1-1サーバー接続不能なし
1-2通信タイムアウトなし
2-1未認証なし
2-2権限不一致なし
2-3セキュリティ保護なし
2-4バグなし
3-1排他制御エラーPageコンポーネントでAppErrorをcatchして実装する
3-2ユニーク制約違反エラー同上
3-3データが存在しないエラー同上
3-4バリデーションエラー同上
3-5画面機能固有のエラーPageコンポーネントかサービスクラスで実装する

共通処理としての実装例

例外クラス

まずは、例外クラスを作って、 error を catch したときに、区別できるようにします。

export namespace AppError {
  export function isInstance(error: BaseError, clazz) {
    return error.name && error.name === clazz.name;
  }

  export class BaseError extends Error {
    constructor(message?: string, error?: Error) {
      super(message);
      this.name = 'AppError.BaseError';
      this.message = message;
      if (error) {
        this.stack += `\nCaused by: ${error.message}`;
        if (error.stack) {
          this.stack += `\n${error.stack}`;
        }
      }
    }
  }

  export class ApiError extends BaseError {
    private response: HttpResponseBase;

    constructor(message?: string, response?: HttpResponseBase, error?: Error) {
      super(message, error);
      this.name = 'ApiError';
      this.response = response;
    }
  }

  export class BadRequest extends ApiError {
    constructor(message?: string, response?: HttpResponseBase, error?: Error) {
      super(message, response);
      this.name = 'BadRequest';
    }
  }

  export class Unauthorized extends ApiError {
    constructor(message?: string, response?: HttpResponseBase, error?: Error) {
      super(message, response);
      this.name = 'Unauthorized';
    }
  }

  ・・・

  export class ApiErrorFactory {
    public static getError(res: HttpResponseBase): ApiError {
      let error: ApiError = null;
      switch (res.status) {
        case 400:
          error = new AppError.BadRequest(null, res);
          break;
        case 401:
          error = new AppError.Unauthorized(null, res);
          break;
        case 403:
          error = new AppError.Forbidden(null, res);
          break;

        ・・・
      }
      return error;
    }
  }

※ AppError.isInstanceは Typescript の instanceof が意図した結果を返してくれないので、各エラークラス内に、クラス名を保持するようにして、それと一致するかをチェックするためのユーティリティです。

インターセプター

http通信のエラーを処理するためのインターセプターです。

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  constructor(private router: Router, private alertService: AppAlertService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    if (req.reportProgress) {
      throw new AppError.BaseError('not implements');
    }
    return next.handle(req).pipe(
      map((event: HttpEvent<any>) => {
        if (event instanceof HttpResponse) {
          const err = this.handleAppError(event);
          if (err) {
            throw err;
          }
        }
        return event;
      }),
      catchError((errRes: HttpErrorResponse) => {
        if (errRes.error instanceof ErrorEvent) {
          const message = `An error occurred: ${errRes.error.message}`;
          this.errorLog(message);
          this.alertService.error(message);
        } else {
          const err = this.handleAppError(errRes);
          if (err) {
            throw err;
          }
        }
        return throwError(errRes);
      })
    );
  }

  private handleAppError(event: HttpResponseBase) {
    const err = AppError.ApiErrorFactory.getError(event);
    if (err === null) {
      return err;
    }
    if (AppError.isInstance(err, AppError.Unauthorized)) {
      this.errorLog(err);
      this.router.navigate(['/login']);
      return null;
    }
    if (AppError.isInstance(err, AppError.Forbidden)) {
      this.errorLog(err);
      this.router.navigate(['/error/403']);
      return null;
    }
    if (AppError.isInstance(err, AppError.ServerError)) {
      this.errorLog(err);
      this.router.navigate(['/error/500']);
      return null;
    }
    if (AppError.isInstance(err, AppError.Maintenance)) {
      this.errorLog(err);
      this.router.navigate(['/error/503']);
      return null;
    }
    return err;
  }

  private errorLog(message: string | Error) {
    if (message instanceof Error) {
      const err = message;
      message = `${err.message}: ${err.stack}`;
    }
    console.error(message);
  }
}

アラート表示用のサービス

タイムアウトなどのErrorEventが発生したときには、特定のコンポーネントに依存しないSnackBarで、アラートメッセージを表示する例です。

@Injectable()
export class AppAlertService {
  constructor(private snackBar: MatSnackBar, private zone: NgZone) {}

  error(message: string) {
    this.zone.run(() => {
      const snackBar = this.snackBar.open(message, 'OK', {
        verticalPosition: 'bottom',
        horizontalPosition: 'center',
      });
      snackBar.onAction().subscribe(() => {
        snackBar.dismiss();
      });
    });
  }
}

バグなどのグローバルエラーハンドラー

バグ発生時などの不測のエラーでは、2次被害が出ないように、システムエラーの画面にリダイレクトさせる例です。

@Injectable({
  providedIn: 'root',
})
export class AppErrorHandler implements ErrorHandler {
  constructor(private injector: Injector) {}

  handleError(error) {
    this.errorLog(error);
    const router = this.injector.get(Router);
    const zone = this.injector.get(NgZone);
    zone.run(() => {
      router.navigateByUrl('/error/500');
    });
  }

  private errorLog(message: string | Error) {
    if (message instanceof Error) {
      const err = message;
      message = `${err.message}: ${err.stack}`;
    }
    console.error(message);
  }
}

画面側の実装例

排他制御エラー

ページコンポーネントの〇〇更新サービスを実行する箇所で、例外をcatchする例

return this.hogeService.update(data).pipe(
  map((result) => {・・・}),
  catchError((err) => {
    if (AppError.isInstance(err, AppError.BadRequest)) {
      this.errorMessage = '他のユーザーによって更新されています。最初からやり直してください';
      return EMPTY;
    } else if (AppError.isInstance(err, AppError.NotFound)) {
      this.errorMessage = '他のユーザーによって削除されています。最初からやり直してください';
      return EMPTY;
    }
    return throwError(err);
  })
);

ユニーク制約違反エラー

ページコンポーネントの〇〇保存サービスを実行する箇所で、例外をcatchする例

return this.hogeService.update(data).pipe(
  map((result) => {・・・}),
  catchError((err) => {
    if (AppError.isInstance(err, AppError.Conflict)) {
      this.errorMessage = 'キーが重複しました。最初からやり直してください';
      return EMPTY;
    }
    return throwError(err);
  })
);

データが存在しないエラー

データ存在しないときに、どうするのかは、画面固有の仕様として策定する必要があります。
よくあるパターンを上げてみます。

  • システム例外として、404の画面に遷移する例
  return this.hogeService.get(data).pipe(
    map((result) => {・・・}),
    catchError((err) => {
      if (AppError.isInstance(err, AppError.NotFound)) {
        this.router.navigate(['/error/404']);
        return EMPTY;
      }
      return throwError(err);
    })
  );
  • データがないときには、別のページに遷移する例(例えば遷移前の画面が一覧画面だったら、その画面にリダイレクトする)
  return this.hogeService.get(data).pipe(
    map((result) => {・・・}),
    catchError((err) => {
      if (AppError.isInstance(err, AppError.NotFound)) {
        this.router.navigate(['/hoges/list']);
        return EMPTY;
      }
      return throwError(err);
    })
  );
  • データがないときは、エラーメッセージを表示する
  return this.hogeService.get(data).pipe(
    map((result) => {・・・}),
    catchError((err) => {
      if (AppError.isInstance(err, AppError.NotFound)) {
        this.errorMessage = 'データが存在しません';
        return EMPTY;
      }
      return throwError(err);
    })
  );

バリデーションエラー

サーバー側でバリデーションエラーが発生することもあるでしょう。 コンポーネント内のエラーメッセージにセットする例です。

return this.hogeService.register(data).pipe(
  map((result) => {・・・}),
  catchError((err) => {
    if (AppError.isInstance(err, AppError.BadRequest)) {
      this.errorMessage = 'XXXXXXXXXXXX';
      return EMPTY;
    }
    return throwError(err);
  })
);

The post Angularのエラー処理について考える(実装編) first appeared on 株式会社Altus-Five.

]]>
/blog/2019/04/10/angular-error-hadling-implement/feed/ 0
Angularのエラー処理について考える(設計編) /blog/2019/03/30/angular-error-hadling-design/ /blog/2019/03/30/angular-error-hadling-design/#respond Sat, 30 Mar 2019 14:51:00 +0000 http://43.207.2.176/?p=300 システムの開発をしていると、約30%は、エラー処理の対策になると聞いたことがあります。30%という数字は、根拠がないので置いとくとして、システムを構築する上で、エラーに対処することは、 非常に重要な設計要素であることは、 […]

The post Angularのエラー処理について考える(設計編) first appeared on 株式会社Altus-Five.

]]>
システムの開発をしていると、約30%は、エラー処理の対策になると聞いたことがあります。
30%という数字は、根拠がないので置いとくとして、システムを構築する上で、エラーに対処することは、 非常に重要な設計要素であることは、間違いありません。

本記事は、私がAngularの開発を通じてエラー処理について、考えたことを整理してみました。 尚、モバイルアプリなども含めると、いろいろ複雑になるので、デスクトップ利用に限定された、業務システムということで、検討範囲を絞り込んでいます。

エラー処理設計の目的

エラー処理を設計するにあたり、検討しておきたいことを挙げてみました。

  • 発生し得るすべてのエラーを明らかにする
  • エラー発生時の振る舞いが統一されている
  • 各画面への重複した設計記述および実装を減らす
  • 漏れなく穴をふさぐ

このあたりは、Angular(SPA)だからというよりも、システムとして共通の目的ですね。

エラー分類

プロジェクトの開始時点では、すべてのエラーを洗い出すことが難しいかもしれないので、典型的なエラーとして分類するところから始めます。
そして、画面の設計、実装過程で、見つかったエラーを、この分類に当てはめて、具体的な仕様として定義していくのが、良いかと思います。
この分類にハマらないものが出てきたら、その都度検討して、新しい分類を追加していくことにします。

(エラー分類1) 致命的エラー

システムが動作する前提となるハードウェアあるいはミドルウェアが異常な状態を示すエラー。

  • 1-1 localStorageのエラー ログイン状態の保持などに、localStorageを使用する場合があり、CookieをOffにしている場合や、一部ブラウザのプライベートモードの場合にエラーになることがある。
  • 1-2 メンテナンス中 APIサーバーがメンテナンス中の状態。

APIサーバーに接続できないときのエラーとかは、ここに分類されてもよいかもしれませんが、瞬間的なネットワーク断は、特にモバイルアプリなどでは、よくあることで、致命的ではないため、ここには分類しませんでした。

🌞 エラー時の振る舞い例
2つくらい考えられると思います。

  • ログイン画面を表示してエラーメッセージとして、エラー原因を表示する
  • システムエラーの500画面、あるいはメンテナンス中の503画面を表示して、同じくエラー原因を表示する

どちらにしても、エラー原因をメッセージとして表示することで、利用者が次のアクションが取れるようにしておくと良いでしょう。

(エラー分類2) 業務継続不可能なエラー

利用する機能の前提要件を満たしていなくて、業務が行えないエラー。

  • 2-1 未認証 ログインしないで、業務画面(URL)にアクセスがあった場合。
  • 2-2 権限不一致 アクセス権限のない、業務画面(URL)にアクセスがあった場合
  • 2-3 セキュリティ保護 XSRF、不正操作、認証時間のタイムアウトなど、セキュリティ保護策によるエラー
  • 2-4 バグ バグによって想定外の状態になった場合

🌞 エラー時の振る舞い例
未認証の場合は、ログイン画面にリダイレクトされるのが、よくある仕様だと思います。
権限不一致とセキュリティ保護は、403画面を表示して不正操作のヒントを与えないのが良いと思います。 バグも、2次被害の恐れがあるので、500画面を表示して続く操作が出来ないようにするのが良いでしょう。

(エラー分類3) 業務継続可能なエラー

発生したエラーは、エラーを除去(解除)することで、業務が継続できるエラー。

  • 3-1 排他制御エラー 複数端末で同一データに同時更新があった場合のエラー。
  • 3-2 ユニーク制約エラー 同一データ(同一キー)が同時に登録された場合のエラー。
  • 3-3 データが存在しないエラー 存在するハズのデータが無かった場合のエラー。 例えば、一覧で選択して詳細ページに遷移するようなケースで、選択したデータが、他の端末で一瞬早く削除された場合など。
  • 3-4 バリデーションエラー 入力値のエラー。 入力値を修正することで、業務継続が可能。
  • 3-5 通信タイムアウト タイムアウトが発生して、期待するデータの取得あるいは、更新ができなかった状態。
  • 3-6 サーバー接続不能 APIサーバーに接続できない状態。
    クライアント側でネットワーク障害があった場合などが想定されます。
    特に、モバイルアプリの場合は、ネットワーク断の状態は、日常的に発生するエラーなので、リトライ処理で自動回復させる仕組みを用意することもあると思います。
    担当したプロジェクトは、デスクトップアプリに限定されていたので、APIサーバーに接続できないときのリトライは行わずに、ユーザーの再操作を期待するUIにしました。
  • 3-7 画面機能固有のエラー 画面機能によるので、個別画面の仕様として規定する。

🌞 エラー時の振る舞い例
排他制御エラーとユニーク制約エラーは、操作画面上にエラーメッセージを表示して、やり直しが出来るようにします。 バリデーションも同じです。再入力するためのガイダンスを表示します。 通信タイムアウトについては、システムの特性によります。私が担当したプロジェクトでは、通信がタイムアウトしたことを、Snackbarで表示させて、再操作を即すようなUIにしました。 画面機能固有のエラーは、ここでは割愛します。

エラー発生元と処理方式

エラーが発生する場所としては、次が考えられます。

  • Web APIの実行時
  • localStorage アクセス時
  • バグ
  • 機能固有の仕様

それぞれの発生元別に、エラーを整理します。

API実行時のエラー処理

APIの実行では、2種類のエラーが発生する可能性があります。
@see https://angular.jp/guide/http#%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%8F%E3%83%B3%E3%83%89%E3%83%AA%E3%83%B3%E3%82%B0

  • エラーステータス APIがHTTPのステータスコードでエラーを返す
  • ErrorEvent ネットワークエラーやrxjs過程のバグでErrorEventが発生する

実装としては、http通信に関連したエラー処理はインターセプターで行い、画面個別での対処が必要なエラー(例えばunique制約の発生など)に対しては、例外をthrowすることで画面側に処理を連携する方式としました。

@see https://angular.jp/guide/http#%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%81%A8%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%82%BB%E3%83%97%E3%83%88

インターセプター内でのhttpステータスコード毎の処理

  • 400 Bad Request
    パラメータのエラーや、validationエラーなどが想定される。
    インターセプターからは、独自の例外クラス AppError.BadRequest をthrowする。
    Page Componentの中では、例外をcatchしてエラー内容を画面に反映する。
  • 401 Unauthorized
    インターセプター内で、@angular/router/Router#navigate にloginのURLをセットして、ログイン画面に遷移する。
  • 403 Forbidden
    アクセス不可のエラーなので、403画面に遷移する。
  • 404 Not Found
    詳細画面や編集画面で対象データが存在しない場合などが想定される。
    インターセプターからは、独自の例外クラス AppError.NotFoundをthrowする。
    Page Componentの中では、例外をcatchしてエラー内容を画面に反映する。
    一覧画面で一覧の内容がない場合は、画面内に反映する。
    詳細画面や編集画面でURL内にIDが含まれる場合は、@angular/router/Router#navigate に404画面のURLをセットして、404画面に遷移する。
    一覧画面の親データ(例えばアカウント一覧の組織自体が無い)が無い場合も同様に404画面に遷移する
  • 409 Conflict
    データ登録で一意性制約違反や排他エラーが発生した場合などが想定される。
    インターセプターからは、 AppError.Conflict を throw する。
    Page Componentの中でcatchしてエラーに対応した処理を実行する。
  • 500 Internal Server Error
    このエラーは、バグなので、システムエラーの500画面に遷移する。
  • 503 Maintenance
    このエラーも、メンテナンス中の503画面に遷移する。

※AppError は独自の例外クラスを作成してあるものとします

ErrorEvent毎の処理

同じく、インターセプターでのErrorEvent発生時の処理です。

  • タイムアウト
    アラートをSnackbarで表示する。
  • 接続不能
    同じく、Snackbarで表示する。

localStorage アクセス時のエラー処理

エラー分類1の致命的なエラーの処理を行う。

バグのエラー処理

バグは、globalエラーハンドラーで処理することとして、画面側では、例外をcatchしない。

画面固有のエラー処理

エラーの共通仕様としては、定義しないで、画面あるいは、サービスクラスで実装する。


実装編の記事も読んでみて下さい。

The post Angularのエラー処理について考える(設計編) first appeared on 株式会社Altus-Five.

]]>
/blog/2019/03/30/angular-error-hadling-design/feed/ 0