TypeScriptでStripe SDKがthrowするエラーを型安全に処理する方法


この記事は、[TypeScript Advent Calendar 2022](https://qiita.com/advent-calendar/2022/typescript) 20日目の記事です。

Stripeをシステムに組み込む場合、Stripe APIまたはSDKがThrowするさまざまなエラーを処理する必要があります。

そしてTypeScriptの場合、`try ~ catch`でキャッチしたエラーの型が`unknown`になるため、適切にハンドリングするか、`as`などを利用して対応します。

この記事では、Stripe SDKからのエラーであるかを判別する方法などを紹介します。

## Stripe SDKのエラーレスポンスは、`StripeError`を拡張している

Stripe SDKがThrowするStripeに関するエラーは`Error`をextendした`StripeError`クラスを基にしています。

```ts
/**
 * StripeError is the base error from which all other more specific Stripe errors derive.
 * Specifically for errors returned from Stripe's REST API.
 */
class StripeError extends Error {
  readonly message: string;
  readonly type: string;
  readonly raw: unknown;
  readonly rawType?: RawErrorType;
  readonly headers?: {[header: string]: string};
  readonly requestId?: string;
```

https://github.com/stripe/stripe-node/blob/master/src/Error.ts#L6

このクラスをさらにextendして、カードに関するエラーやAPIキーの権限といった様々なエラーを表現しています。

https://github.com/stripe/stripe-node/blob/master/src/Error.ts#L80-L149

## `e instanceof Stripe.errors.StripeError`でStripe SDK由来のエラーか否かを判別する

Node.jsのStripe SDKでは、`StripeError`をベースにエラーオブジェクトが作成されています。

そのため、`instanceof StripeError`でStripe APIに関するエラーかを判別できます。

```ts
  try {
    await stripe.customers.retrieve('cus_dummy')
  } catch (e) {
    if (e instanceof Stripe.errors.StripeError) {
      console.log(`[Error: ${e.code}] ${e.message}`)
    } else {
      console.log(e)
    }
  }
```

このコードを実行すると、`[Error: resource_missing] No such customer: 'cus_dummy'`が表示されます。

また、`StripeError`に所属するプロパティであれば、VS CodeなどのIDEで入力補完が利用できます。

![スクリーンショット 2022-11-15 14.37.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/af781d15-56ed-91c0-2927-0e1585fde61a.png)

## 特定のエラーケースのみハンドルする

「リクエストパラメータが不完全なケース」や「認証エラー」など、特定のケースのみcatchしたいケースも存在します。

その場合は、処理したいエラーコードに対応するエラークラスが存在するかを確認して、`instanceof`の分岐を設定することができます。

参考として、2022/11時点のエラークラス生成処理部分のコードを引用しました。

```ts
  static generate(rawStripeError: StripeRawError): StripeError {
    switch (rawStripeError.type) {
      case 'card_error':
        return new StripeCardError(rawStripeError);
      case 'invalid_request_error':
        return new StripeInvalidRequestError(rawStripeError);
      case 'api_error':
        return new StripeAPIError(rawStripeError);
      case 'authentication_error':
        return new StripeAuthenticationError(rawStripeError);
      case 'rate_limit_error':
        return new StripeRateLimitError(rawStripeError);
      case 'idempotency_error':
        return new StripeIdempotencyError(rawStripeError);
      case 'invalid_grant':
        return new StripeInvalidGrantError(rawStripeError);
      default:
        return new StripeUnknownError(rawStripeError);
    }
  }
```

https://github.com/stripe/stripe-node/blob/master/src/Error.ts#L56-L76

以下の例では、`InvalidRequest Error`を「ユーザーが入力したデータによるエラー」と仮定し、それ以外を内部エラーとしてレスポンスするAPIを作成しています。

```ts
  try {
    await stripe.customers.retrieve('cus_dummy')
  } catch (e) {
    // リクエスト内容が不完全なので、HTTP 400を返す
    if (e instanceof Stripe.errors.StripeInvalidRequestError) {
      return res.status(400).json({
        code: e.code,
        message: e.message,
      })
    // 想定外のエラーがStripeから来ている可能性が高いため、HTTP 500で内部エラーとする
    } else if (e instanceof Stripe.errors.StripeError) {
      return res.status(500).json({
        code: e.code,
        message: "Internal server error"
      })
    // Stripeが関係しないエラーが発生したため、これも内部エラーとする
    } else {
      return res.status(500).json({
        message: "Internal server error"
      })  
    }
  }
```

## Stripe APIエラーを型安全にハンドルし、レポーティングやユーザーへのフィードバックに活用しよう

StripeのAPIエラーレスポンスには、「関係性の高いと思われるドキュメントのURL」や「ダッシュボードでAPIリクエスト内容を確認できるページのURL」など、様々な追加情報が含まれています。

https://qiita.com/hideokamoto/items/5505d8fbd717853d2c4b

型安全にStripeのAPIエラーを処理することで、これらの追加情報をユーザーへのフィードバックやトラッキングツールに送信し、UXやバグ改善のフローを効率化しましょう。