エラーのラッピング

注:本記事は元々 Qiita で公開していたものの再掲です。

Go 1.13 で追加された、エラーラッピングについて。

概要

  • エラーを他のエラーでラップすることが可能になる
    • 元のエラーの型やフィールド値を保持しつつ新しいエラーを生成できる
    • ラップされたエラーは、あとから取り出すことが可能
  • 呼び出し先の深い場所で起きたエラーの種類を判別しやすくなる

使い方

エラーをラップする

innerErr := fmt.Errorf("Inner Error")
newErr := fmt.Errorf("Wrapped: %w", innerErr)

従来使用してきた fmt.Errorf() でできる。 通常は fmt.Errorf()errors.errorString 側のエラーを返すが、以下の条件を満たす場合、fmt.wrapError 型のエラーを返す。

  • フォーマット指定子に %w を使用する。文字列内での位置は問わない。
  • その %w を置き換えるのが error 型である(上のinnerErr)

fmt.wrapError とは

ラップ機能が追加された error 型。

通常 errors.New() が返す errorString 型は、エラー内容を単純な文字列として保持している。 この方法でエラーをネストさせる場合、エラー文字列を連結していくという方法をとらざるを得ない。結果的に以下のようになると思う。

err := fmt.Errorf("Inner Error")
newErr := fmt.Errorf("Wrapped: %s", err)
fmt.Println(newErr)   // 'Wrapped: Inner Error'

これだと、エラーの種類の判別がやや面倒になってしまう。(上記で言うと、Inner Error の発生がエラーの根本原因になっているかどうかを判定する場面など。文字列のパースが必要になる。)

しかし fmt.wrapError は以下の仕組みを持っているため、元のエラーを単独で管理することが可能となる。

  • fmt.wrapError 構造体には、元のエラーを保持するフィールドがある(err)
type wrapError struct {
    msg string
    err error       // ここに元のエラーが入る
}
  • Unwrap() を実装している。これはラップされた元のエラーを返す。
func (e *wrapError) Unwrap() error {
    return e.err
}

このため、アンラップ時に元のエラーを取り出すことが簡単になっている

エラーをアンラップする

あるエラーからラップされた中身のエラーを取り出すには、errors.Unwrap() を使う。

errors.Unwrap(newErr error) error

iErr := fmt.Errorf("Inner Error")
newErr := fmt.Errorf("Wrapped: %w", iErr)   // "Wrapped: Inner Error"
innerErr := errors.Unwrap(newErr)   // "Inner Error"
  • Unwrap() の引数で渡されたエラーが Unwrap() を実装していれば、その戻り値を返す
    • newErrfmt.wrapError 型。これは Unwrap() を実装していて、newErr.erriErr を返す。
  • Unwrap() が未実装なら nil を返す

エラーを探索する

ラップされた一連のエラーの中から、目的のエラーを探し出すことができる。以下の2つのメソッドが提供されている。

  • errors.Is(err, target error)
  • errors.As(err error, target interface{}

errors.Is : 特定のエラーを探す

  • エラーチェーンの中に、特定のエラーが存在するかどうかを調べる。存在すると判定される条件は以下のいずれか。
    • err == target が成立する err がエラーチェーンに存在する
    • errIs() が実装されていて、 err.Is(target)true を返す err がエラーチェーンに存在する
  • 例えば A -> B -> C の順にラップしたとして、A をラップしているかどうかを判定できる。

err == target が成立する err がエラーチェーンに存在する例

https://play.golang.org/p/GoL2silcyCF

func main() {
    errA := fmt.Errorf("First Error")   // これをラップしているかどうかを判定したい

    errB := fmt.Errorf("Wrapped: %w", errA)   // B <- A
    fmt.Printf("errB wraps errA = %v\n", errors.Is(errB, errA))

    errC := fmt.Errorf("Wrapped: %w", errB)   // C <- B <- A
    fmt.Printf("errC wraps errA = %v\n", errors.Is(errC, errA))

    errX := fmt.Errorf("Another error")       // A をラップしてない
    fmt.Printf("errX wraps errA = %v\n", errors.Is(errX, errA))
}

実行結果

errB wraps errA = true
errC wraps errA = true
errX wraps errA = false

errA == errB が成立するためには、同一インスタンスである必要がある。以下の例で、エラー文字列が同一で型も同一のインスタンスを2つ作成しているが、別インスタンスの場合は同一と判定されない。文字列が同じだから同一、ということではないので注意。

https://play.golang.org/p/X71IvCmYi6s

   errA := fmt.Errorf("Error Y")
    errB := fmt.Errorf("Error Y")    // errA と全く同じ文字列
 
    errX := fmt.Errorf("Wrap: %w", errA)
 
    fmt.Printf("errX wraps errA : %v\n", errors.Is(errX, errA))
    fmt.Printf("errX wraps errB : %v\n", errors.Is(errX, errB))

実行結果

errX wraps errA : true
errX wraps errB : false

なので通常は、グローバル空間でエラーオブジェクトを生成しておき、それを使い回すという方法で比較することになると思う。

err.Is() が実装されていて、true を返す例

2つの err が通常なら等価と判断されない場合であっても、任意の条件で同一とみなしたい場合は、Is() を実装すると良い。 以下の例では、errAerrB が同一インスタンスどころか同一の型ですらないが、結果的に同一のエラーと判定させている。

https://play.golang.org/p/sZtPmqjg7Ms

type myError struct {
    error
}

// 文字列が一致していれば同一と判断
func (e myError) Is(target error) bool {
    return e.Error() == target.Error()
}

func main() {
    errA := &myError{fmt.Errorf("Error E")}
    errB := fmt.Errorf("Error E")

    errX := fmt.Errorf("Wrapped %w", errA)
    fmt.Printf("errX wraps errB : %v\n", errors.Is(errX, errB))
}

実行結果

errX wraps errB : true

errXerrA をラップしている。errAerrB は型が異なるが、Is() で等価と判断されている。 結果 errXerrB に相当するものをラップしている、とみなされる。

errors.Is() は、特定のエラーがエラーチェーン内にあるかどうかを判定するだけ。その特定のエラーを取得し、さらになんらかの形で利用したい場合は、この次の errors.As を使用すると良い。

errors.As : 特定の型を持つエラーが含まれるか判定し、それを取得する

errors.As(err error, target interface{}) を使う。 エラーチェーンを辿っていき、target に代入可能なエラーが見つかったらそれを target で受け取る。 戻り値は bool で、目的のエラーが見つかった場合に true、見つからなければ false が返る。 エラーに時刻などの付加情報を追加し、それを利用したい場合はこちらが便利。

(代入可能の詳細は こちら )

target は以下のいずれかを指定可能。

  • Interface 型へのポインタ
  • error 型を実装した型へのポインタ

error 型を実装した独自のエラー型を使用した例。

https://play.golang.org/p/fr_jLSk6gd_z

type myError struct {
    t time.Time
}

func (e myError) Error() string {
    return fmt.Sprintf("[%s] myError found!", e.t.Format("2006/1/2 15:04:05"))
}

func main() {
    var innerErr myError

    err := wrap1()

    if errors.As(err, &innerErr) {
        fmt.Printf("%s\n", innerErr)
    } else {
        fmt.Printf("Unknown error : %s\n", err)
    }
}

func wrap1() error {
    err := wrap2()
    return fmt.Errorf("wrap1: %w", err)
}

func wrap2() error {
    return myError{t: time.Now()}
}

余談

fmt.Errorf%w を2つ以上指定するとどうなるか

(追記: 2022/12/07)

go1.20 から、2つ以上のエラーのラッピングが可能となるようです。

https://github.com/golang/go/issues/53435 https://twitter.com/inancgumus/status/1597571791941414912

以下の記述は、それ以前のエラーラッピングに関するものです。


2つ以上のエラーを同時にラップしようとするとどうなるのか。結論から言うとラップできず通常の errors.errorString が返ってくる。当然 Unwrap() でラップしたエラーを取り出すこともできなくなる。

https://play.golang.org/p/NqfHjIzApCQ

   errA := fmt.Errorf("Error A")
    errB := fmt.Errorf("Error B")
    errC := fmt.Errorf("Error C")

    errX := fmt.Errorf("1: %w, 2: %w, 3: %w", errA, errB, errC)
    fmt.Printf("%#v\n", errX)

    errU := errors.Unwrap(errX)
    fmt.Println(errU)

実行結果

&errors.errorString{s:"1: Error A, 2: %!w(*errors.errorString=&{Error B}), 3: %!w(*errors.errorString=&{Error C})"}
<nil>

ラップしたエラーの型がもはや fmt.wrapError ではなく errors.errorString になっている。

そもそも複数のエラーを指定することは意図的に考慮の対象外となっている模様。うっかりエラーを見落とすなど想定外の挙動に繋がりかねないので注意する必要がある。

https://github.com/golang/go/blob/688aa748579f07552a50d2534eccb16afda4174b/src/fmt/print.go#L579-L587

// It is invalid to use %w other than with Errorf, more than once, or with a non-error arg.