tRPCでエラーを型安全に扱う

私はtRPCのBig Fanである。

End-to-Endで型安全になる開発体験の良さは一度使うともはや後戻りできない中毒性がある。下手なテストを量産するよりも型をちゃんとする方がずっと予後がいい。

ときに、エラー情報を型をつけたままフロントエンドに返したい場合、tRPCをもってしても少し工夫が必要だったので概要をメモしておく。

バックエンドの設定

まずはバックエンドでカスタムエラーを定義する。カスタムエラーにはフロントエンドに返す情報(以下、ペイロード)をプロパティに持たせておく。 このとき、ペイロードの型をタグ付きユニオンで定義しておき、フロントエンドにその型情報を公開する最大のポイントだ。

次にエラーフォーマッターを設定する。フォーマッターはtRPCをイニシャライズする際に設定するようになっている。エラー発生時にどのような形でフロントエンドに情報が送られるかはここで決まる。

export const t = initTRPC.create({
  errorFormatter({ error, shape }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        // こいつを追加してやる。名前は任意。
        customErrorPayload:
          error.cause instanceof CustomError ? error.cause.payload : null,
      },
    }
  },
})

これでバックエンドの設定は完了だ。フロントエンドにエラーを返したいと思った時は、以下のような感じでthrowすれば良い。こうすればtRPCがよしなにフロントエンドに情報を送ってくれる。

/**
 * 毎回TRPCErrorでラップするのが面倒なので作成したutil関数
 */
export const throwCustomError = (payload: CustomErrorPayload) => {
  throw new TRPCError({
    code: 'BAD_REQUEST',
    cause: new CustomError(payload),
  })
}

フロントエンドの設定

フロントエンドでは、エラーの中身を検査して、カスタムエラーペイロードを取り出すutil関数を作成しておく。

使い方

あとはフロントエンドのエラーハンドリングしたい箇所でペイロードを取り出して、型に応じた処理をすれば良い。ちなみに以下の例だとerrorPayload?.type === 'と打った時点で候補が出てくるし、if文の内部ではerrorPayload.rと打ち込んだ時点で候補が出てくる。

めでたくして黒部トンネルの開通である(バックエンドからフロントエンドに型安全にエラーを返すことができている)。fin。

const e = new Error('検査したいエラー')

const errorPayload = unwrapCustomErrorPayload(e)
if (errorPayload?.type === 'product.date_limit_exceeded') {
  console.log(errorPayload.remainingDate)
}