エラー処理
呼び出し元が対処できる想定内の失敗と、プログラムの前提が崩れた想定外の失敗を分けます。
想定内の失敗
未存在、入力競合、権限不足など、呼び出し元が分岐する失敗は、標準のErrorを継承した具体的なエラーを値として返します。バックエンドではT | Errorを共通の戻り値にでき、呼び出し側は具体的なエラーをinstanceofで判別します。すべての失敗を独自コンテナへ包む汎用Result型は使いません。Effectのようなライブラリも、機能面の利点より外部依存と実装の複雑さが上回るため採用しません。
class EmailAlreadyUsedError extends Error {}
const registerUser = async (
input: RegisterUserInput,
): Promise<User | Error> => {
if (await emailExists(input.email)) {
return new EmailAlreadyUsedError()
}
return saveUser(input)
}
const result = await registerUser(input)
if (result instanceof EmailAlreadyUsedError) {
return c.json({ code: "EMAIL_ALREADY_USED" }, 409)
}
if (result instanceof Error) {
return toErrorResponse(result)
}
return c.json(result)
公開する戻り値ですべての失敗を網羅する必要がある場合は、T | ValidationError | ConflictErrorのように具体型を列挙します。失敗しない補助関数までUnion型にしません。エラー型は分岐に必要な情報だけを持ち、利用者向けメッセージとは分離します。
境界での変換
外部ライブラリ、データベース、外部APIが投げる例外はInfrastructureの境界で捕捉し、Errorの値へ変換します。Applicationが判断できる失敗は具体的なエラーへ変換し、判断できない技術的な失敗は意味を失わないまま上位へ返します。調査に必要な元の例外はcauseなどで保持し、ログにだけ残します。try-catchはネストさせず、内側で捕捉が必要になった処理は専用の関数へ切り出します。
エラーは次の順で意味を変換します。
- Infrastructureが技術的な失敗を検出し、
Errorとして返す - Applicationが判断できる失敗を、業務上の具体的なエラーへ変換する
- Interfaceが既知のエラーをHTTPステータスや画面表示へ変換し、それ以外を共通のエラーハンドラへ渡す
認証・認可の判定に失敗した場合は許可せず、利用者へ内部情報を返しません。
想定外の失敗
到達不能な状態、契約違反、プログラミングミスは、途中で成功値や空の値へ変換せず、Errorのまま最上位の共通ハンドラへ到達させます。ログは責任を持つ境界で一度だけ記録し、各層で同じエラーを重複記録しません。
並列処理が全件成功を要求するならPromise.all、一部成功を扱う要件があるならPromise.allSettledを使います。失敗を無視する目的でallSettledを選びません。
ReactのError Boundaryはレンダー中やSuspenseの失敗を扱う境界です。イベントハンドラや任意の非同期処理の失敗は捕捉しないため、mutationなどの失敗は実行箇所で明示的に扱います。