エラー型設計レビュー:判定可能性と責務分離からの評価

Go言語におけるerrorは非常に柔軟なインタフェース型であり、標準の errors.New() もあれば、開発者定義のカスタム型も容易に作成できます。
だがその柔軟さゆえ、レビューの現場では曖昧なエラー型設計意図の不明瞭なカスタム型が頻出します。

この記事では、レビューアーとしてエラー型設計をどのように評価すべきかを、良い実装例と問題例を交えて整理します。


良い実装例:判定可能性と利用箇所を意識したカスタムエラー型設計

まずは、レビューアーが「設計意図が明確で良い」と評価できる実装例から見ていきます。

シナリオ

ここではユーザー認証の失敗を取り扱うシンプルなAPIサーバを題材にします。失敗原因に応じて、ログ出力・再試行可否・UIメッセージを適切に制御する設計です。

package auth

import (
	"errors"
	"fmt"
)

// ベースとなる抽象エラー型
type AuthError interface {
	error
	Temporary() bool
	PublicMessage() string
}

// 実際の具体型その1:資格情報エラー(ユーザ起因)
type CredentialError struct {
	Username string
}

func (e *CredentialError) Error() string {
	return fmt.Sprintf("invalid credentials for user: %s", e.Username)
}

func (e *CredentialError) Temporary() bool {
	return false
}

func (e *CredentialError) PublicMessage() string {
	return "ユーザー名またはパスワードが正しくありません"
}

// 実際の具体型その2:外部IDプロバイダ障害(システム起因)
type IDPUnavailable struct {
	Provider string
	Wrapped  error
}

func (e *IDPUnavailable) Error() string {
	return fmt.Sprintf("identity provider %s unavailable: %v", e.Provider, e.Wrapped)
}

func (e *IDPUnavailable) Temporary() bool {
	return true
}

func (e *IDPUnavailable) PublicMessage() string {
	return "外部認証サービスに接続できませんでした。しばらくして再試行してください。"
}

// 呼び出し側の使用例
func Authenticate(username, password string) (string, error) {
	if username == "" {
		return "", &CredentialError{Username: username}
	}
	// 省略:IDPへのアクセス失敗例
	return "user-token", nil
}

技術解説

この設計では以下の特徴が備わっています。

  • 分類可能性

    • エラーの種類を責務ベースで分離(ユーザー入力エラー vs. システム障害)
  • 判定可能性

    • インタフェースAuthErrorを用意し、ラップ可能性・キャスト容易性を確保
  • UI・ログ分離

    • PublicMessage() でユーザ表示文を分離し、ログには内部詳細を含める
  • リトライ制御

    • Temporary() の有無で再試行制御の自動化が可能
  • 拡張容易性

    • 新たな具体型の追加が自然な形で行える構造

このように「誰に何を伝えるエラーか」「復旧可能性があるのか」まで設計されたエラー型は、レビューでも高評価となります。


良くない実装例

では、レビュー対象として実際に問題となる典型的な悪い例を提示します。
ここでは、設計意図が不明確で拡張性に欠けるコードを例にします。

package auth

import (
	"errors"
)

var (
	ErrInvalidCredentials = errors.New("invalid credentials")
	ErrServiceUnavailable = errors.New("service unavailable")
)

func Authenticate(username, password string) (string, error) {
	if username == "" {
		return "", ErrInvalidCredentials
	}
	// IDPへのアクセス失敗時
	return "", ErrServiceUnavailable
}

レビューアーの指摘コメント例を含めて見ていきます。

var (
	ErrInvalidCredentials = errors.New("invalid credentials")
@Reviewer
エラー原因が複数ある場合でも文字列だけで表現しており、判定や分類ができません。呼び出し側で型判定可能なカスタム型を導入しましょう。
ErrServiceUnavailable = errors.New("service unavailable")
@Reviewer
再試行可否など復旧可能性を示す情報が含まれていません。Temporary判定やPublicMessage分離の設計を検討してください。
)
func Authenticate(username, password string) (string, error) {
	if username == "" {
		return "", ErrInvalidCredentials
	}
	return "", ErrServiceUnavailable
}

レビュー観点チェックリスト

チェック観点 確認内容
設計意図の説明性 このカスタムエラー型は何のために定義したのか明確か?
分類と責務の分離 ユーザー入力系、システム障害系、再試行可否が整理されているか?
判定可能性 errors.Is, errors.As 等で柔軟に判定可能か?
利用箇所の拡張性 新たな具体型を自然に追加できる構造になっているか?
UI表示と内部ログの分離 ユーザ向けエラーメッセージがログ詳細と混在していないか?
責務の集中 呼び出し側の再試行判断・UI制御がシンプルになっているか?

あとがき

Goのエラーハンドリングは柔軟であるがゆえに、レビュー観点の粒度を上げないと「動くけど設計として弱いエラー型」が量産されがちです。
エラー設計レビューでは、判定可能性・責務分離・復旧可能性の設計意図が説明できる構造になっているかを中心に読み解くと精度が上がります。
カスタムエラーは作るのが目的ではなく、「呼び出し側が安全に判断できる構造化された情報提供」が本質です。