こんにちは!フリーランスエンジニアのこたろうです。
エラーチェーンについて、学びで得た知見を共有します。
エラーチェーンとは
エラーチェーンは、マトリョーシカ(入れ子人形)のように、エラー情報を階層的に保持する仕組みです:
// データベースのエラーを表す構造体
type DBError struct {
Err error // 内部エラー(より深い層のエラー)
Query string // 問題のあったSQL文
}
// バリデーションのエラーを表す構造体
type ValidationError struct {
Err error // 内部エラー(DBErrorなども含められる)
Field string // エラーが発生したフィールド名
}
// 実際の使用例
func createUser(user *User) error {
// 1. バリデーションチェック
if err := validateEmail(user.Email); err != nil {
// EmailのバリデーションエラーをDBエラーでラップ
dbErr := &DBError{
Err: err,
Query: "SELECT * FROM users WHERE email = ?",
}
// さらにバリデーションエラーでラップ
return &ValidationError{
Err: dbErr,
Field: "email",
}
}
return nil
}
// エラーが発生した場合の中身
// ValidationError {
// Field: "email"
// Err: DBError {
// Query: "SELECT * FROM users WHERE email = ?"
// Err: "invalid email format"
// }
// }
このように階層構造にすることで:
- エラーの発生場所が明確になる(バリデーション→DB→元のエラー)
- 各層で必要な情報を付加できる(クエリ内容、フィールド名など)
- デバッグ時に問題を追跡しやすくなる
- エラーの種類に応じた適切な処理が書きやすくなる
エラーチェーンは、エラーが発生した際の「現場検証の記録」のように、問題が起きた状況を詳細に記録する仕組みといえます。
errors.Unwrapの活用
エラーチェーンは玉ねぎのような層構造です。errors.Unwrap
を使うと、各層のエラー情報を1つずつ確認できます:
// エラーが発生する関数の例
func queryUser() error {
err := db.Query("SELECT * FROM users")
if err != nil {
return &DBError{ // 外側:DBエラー
Err: &ValidationError{ // 中間:検証エラー
Err: err, // 中心:元のエラー
Field: "email",
},
Query: "SELECT * FROM users",
}
}
return nil
}
// エラーを層ごとに解析
func inspectError(err error) {
fmt.Println("エラーを解析します:")
currentError := err // 現在見ているエラー
layer := 1 // 何層目かを表す数字
// エラーがnilになるまで繰り返し
for currentError != nil {
// 現在の層のエラー内容を表示
fmt.Printf("層%d: %v\n", layer, currentError)
// 内側の層のエラーを取り出す
currentError = errors.Unwrap(currentError)
// 次の層へ
layer++
}
}
// 実行例
err := queryUser()
inspectError(err)
// 出力例:
// エラーを解析します:
// 層1: DBエラー: SELECT * FROM users の実行失敗
// 層2: バリデーションエラー: emailフィールドが不正
// 層3: 無効なメールアドレス形式です
このようにerrors.Unwrap
を使うと、エラーの発生箇所から順に、どのような問題が起きたのかを詳しく追跡できます。
Errorメソッドの実装
Error()
メソッドは、エラーをテキストとして表示する際の「フォーマット」を定義します。
各エラー型で独自のフォーマットを設定することで、エラー情報をより分かりやすく表示できます:
// DBError用のエラーメッセージフォーマット
func (e *DBError) Error() string {
return fmt.Sprintf("クエリ '%s' の実行中にエラー発生: %v", e.Query, e.Err)
// 出力例: "クエリ 'SELECT * FROM users' の実行中にエラー発生: connection refused"
}
// ValidationError用のエラーメッセージフォーマット
func (e *ValidationError) Error() string {
return fmt.Sprintf("フィールド '%s' の検証エラー: %v", e.Field, e.Err)
// 出力例: "フィールド 'email' の検証エラー: invalid format"
}
// 実際の使用例
func handleUser(user *User) error {
err := validateAndSave(user)
if err != nil {
log.Printf("エラーが発生: %v", err) // Error()メソッドが呼ばれる
// 出力例:
// エラーが発生: フィールド 'email' の検証エラー: クエリ 'SELECT * FROM users' の実行中にエラー発生: connection refused
}
return err
}
このメソッドが重要な理由:
- エラーの可読性向上
- 開発者が理解しやすい形式でエラーを表示
- エラーの文脈や重要な情報を含められる
- ログ出力との相性
- log.Printf や fmt.Printf でエラーを出力する際に自動的に使用される
- デバッグやモニタリングに役立つ情報を含められる
- エラーチェーンとの連携
- 内部エラー(e.Err)も同様に文字列化される
- エラーチェーン全体を人間が読みやすい形で表示できる
errors.Isによるエラー判定
特定のエラー型を含むかどうかを判定できます:
var ErrNotFound = errors.New("not found")
func handleError(err error) {
if errors.Is(err, ErrNotFound) {
// NotFoundエラーの処理
return
}
// その他のエラー処理
}
errors.Isによる実践的なエラーハンドリング
// 事前に定義しておくエラー
var (
ErrNotFound = errors.New("resource not found")
ErrInvalidInput = errors.New("invalid input")
ErrDatabaseError = errors.New("database error")
)
// DBエラーを表す構造体
type DBError struct {
Err error
Query string
}
func (e *DBError) Error() string {
return fmt.Sprintf("DB error: %s (query: %s)", e.Err, e.Query)
}
// errors.Is()のための実装
func (e *DBError) Is(target error) bool {
return target == ErrDatabaseError
}
// 実装例
func getUserData(id string) (*User, error) {
// バリデーション
if !isValidID(id) {
return nil, fmt.Errorf("%w: invalid id format", ErrInvalidInput)
}
// DB処理
user, err := db.FindUser(id)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("%w: user not found", ErrNotFound)
}
return nil, &DBError{
Err: err,
Query: fmt.Sprintf("SELECT * FROM users WHERE id = %s", id),
}
}
return user, nil
}
// エラーハンドリング
func handleUserRequest(w http.ResponseWriter, r *http.Request) {
user, err := getUserData(r.URL.Query().Get("id"))
if err != nil {
switch {
case errors.Is(err, ErrNotFound):
http.Error(w, "ユーザーが見つかりません", http.StatusNotFound)
case errors.Is(err, ErrInvalidInput):
http.Error(w, "入力値が不正です", http.StatusBadRequest)
case errors.Is(err, ErrDatabaseError):
http.Error(w, "システムエラーが発生しました", http.StatusInternalServerError)
log.Printf("DBエラー詳細: %v\n", err)
default:
http.Error(w, "予期せぬエラーが発生しました", http.StatusInternalServerError)
}
return
}
json.NewEncoder(w).Encode(user)
}
まとめ
エラーチェーンの利点:
- エラーの発生経路を正確に追跡可能
- 詳細なデバッグ情報の保持
- エラーの種類に応じた適切な処理の実装
- ログ出力の充実化
これらの機能を組み合わせることで、メンテナンス性の高いエラーハンドリングが実現できます。
参考文献:
-『Goで作るはじめてのWebアプリケーション改訂版』技術書典