コンテンツにスキップ

カスタムエラーの設定⚓︎

下記の手順では、 TypeScript 標準の Error 型の限界をカバーするために、業務例外を表現するための独自例外クラスを実装します。 加えて、 HTTP 通信に由来する業務例外について、外部ライブラリである Axios に直接依存せず、独自例外クラスに正規化してから扱うための設定例を示します。

カスタムエラーの必要性⚓︎

TypeScript には標準の Error 型が用意されていますが、一般に下記のような欠点があります。

  1. 主要なプロパティ( namemessage 、stack )が、エラーの情報を型レベルで構造化して表現するには不十分です。たとえば HTTP ステータスコードやアプリケーション固有のエラーコードなどの情報を表現できません。

  2. Error 型に対して JSON.stringify() を実行した際に、 name, message, stack がシリアライズされないため、ログ出力に必要な情報が欠落します。

これらの欠点をカバーするため、 Error 型を拡張して、業務例外を表す独自例外クラス(カスタムエラー)を実装する必要があります。

他方で、OpenAPI 仕様書からのクライアントコード生成 で導入した Axios には、 HTTP 通信で発生するエラーを表現するために、 Error 型を拡張した AxiosError 型が用意されています。 AxiosError 型には、リクエスト設定 ( config )、レスポンス ( response )、エラーコード ( code ) などが含まれ、 toJSON() メソッドでこれらを JSON 形式に変換できます。 AxiosError の一般的な構造については、 Axios 公式ドキュメントの Handling Errors を参照ください。

エラーハンドリングに AxiosError 型をそのまま用いることも考えられますが、業務例外の構造が直接外部ライブラリに依存しないクリーンなコードを保つため、 AxiosError 型をカスタムエラーへ変換します。

業務例外の表現方法として、 業務例外が発生する可能性のある関数の戻り値を Result 型で表現し、Error 型の throw を避ける方法も考えられます。 しかし、例外処理方針 - フロントエンドの例外処理方針 で述べたように、 try-catch を用いてハンドリングします。

カスタムエラーを定義する⚓︎

標準の Error 型を拡張して、カスタムエラーの基底クラスとなる抽象クラスを実装します。 これを継承するカスタムエラーの具象クラスには、ログ出力に必要な情報を取り出すために、 toJSON() メソッドの実装を強制します。

カスタムエラーの基底クラス
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export abstract class CustomErrorBase extends Error {
  cause?: Error | null

  constructor(message: string, cause?: Error) {
    super(message)
    // ラップ前のエラーを cause として保持
    this.cause = cause
  }

  abstract toJSON(): Record<string, unknown>
}

HTTPError クラスの定義⚓︎

CustomError クラスを拡張して、 HTTP 通信に起因する業務エラーを定義します。

集約例外ハンドラーの設定 で設定した通り、バックエンドから返却されるエラーレスポンスには、 ProblemDetails に基づくレスポンスボディーを含みます。 よって、 HTTP 通信によって発生しうるエラーを表す HttpError クラスには、AxiosError.response.data を経由して ProblemDetails の情報を格納します。

例外処理方針 - API 通信のエラーレスポンス で述べているように、拡張メンバー exceptionIdexceptionValues を追加で定義しています。

加えて、これらの情報を構造化して出力できるように、 toJSON() メソッドを実装します。

基底クラスを継承した HttpError クラスの例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
export class HttpError extends CustomErrorBase {
  response?: ProblemDetails | null

  constructor(message: string, cause?: Error & { response?: { data?: ProblemDetails } }) {
    super(message, cause)
    this.response = cause?.response?.data ?? null
    this.name = 'HttpError'
  }

  toJSON() {
    return {
      timestamp: new Date().toISOString(),
      name: this.name,
      message: this.message,
      stack: this.stack,
      response: this.response ?? null,
      cause: this.cause ?? null,
    }
  }
}
ProblemDetails の型定義
1
2
3
4
5
6
7
8
9
export interface ProblemDetails {
  detail: string
  exceptionId: string
  exceptionValues: string[]
  instance: string
  status: number
  title: string
  type: string
}

HTTPError の具象クラスの定義⚓︎

HttpError を継承して、 発生しうるエラーを表現する具象クラスを定義します。 バックエンドアプリケーションから連携された OpenAPI 定義書に従って、 必要な具象クラスを定義してください。 下記の例は、 HTTP ステータスコード 404 Not Found に対応する具象クラス NotFoundError の実装例です。

HttpError を継承した具象クラスの例
1
2
3
4
5
6
export class NotFoundError extends HttpError {
  constructor(message: string, cause?: Error) {
    super(message, cause)
    this.name = 'NotFoundError'
  }
}

AxiosError をカスタムエラーに変換する⚓︎

Axios の Interceptors を用いて、 AxiosError をカスタムエラーに変換します。 Axios の Interceptors は、 HTTP リクエストおよびレスポンスが処理される前に、任意のハンドリング処理を挟み込んで実行できる機能です。

この機能を用いて、 HTTP ステータスコードに対応した適切なカスタムエラーへ変換する処理を一箇所に集約して実装します。 HTTP ステータスコードは AxiosErrorresponse.status へ格納されています。

axiosInstance.interceptors.response の実装例を下記に示します。 たとえば下記の例では、response.status が 404 である AxiosError を、NotFoundError に変換して返却しています。

サンプルアプリケーションの Interceptors の実装例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
axiosInstance.interceptors.response.use(
  (response) => response,
  (error) => {
    if (axios.isAxiosError(error)) {
      if (!error.response) {
        return Promise.reject(new NetworkError(error.message, error))
      }
      if (error.response.status === Number(HttpStatusCode.InternalServerError)) {
        return Promise.reject(new ServerError(error.message, error))
      }
      if (error.response.status === Number(HttpStatusCode.Unauthorized)) {
        return Promise.reject(new UnauthorizedError(error.message, error))
      }
      if (error.response.status === Number(HttpStatusCode.NotFound)) {
        return Promise.reject(new NotFoundError(error.message, error))
      }
      if (error.response.status === Number(HttpStatusCode.Conflict)) {
        return Promise.reject(new ConflictError(error.message, error))
      }
      return Promise.reject(new HttpError(error.message, error))
    }
    return Promise.reject(new UnknownError('Unknown Error', error))
  },
)

業務例外が発生しうる処理の呼び出し⚓︎

業務例外が発生する可能性のある処理の呼び出し時は、try-catch で囲みます。 例外を catch したら、 instanceof を使用して、どのカスタムエラーに該当するか検出し、検出したエラーに応じた処理を実行します。 下記は API の呼び出しで NotFoundError が発生した場合に、ユーザーへトースト通知し、別画面へ遷移する例です。

サンプルアプリケーションの実装例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const getItem = async (itemId: number) => {
  try {
    setCurrentItemState(await fetchItem(itemId))
  } catch (error) {
    if (error instanceof NotFoundError) {
      showToast('対象のアイテムが見つかりませんでした。')
      router.push({ name: 'catalog/items' })
    }
  }
}