💊

エラーをオブラートに包んでほしい

2023/12/30に公開

TL;DR;

  • ユーザーに見せるエラーには内部の情報を含めたくない
  • しかし、エラーに含まれている情報を落としたくはない

というときに、エラーをオブラートに包んで、ユーザー向けのエラーを用意しつつ、内部にはエラー情報を含んだままにします。

https://github.com/ikawaha/oblate

エラーの Wrap は便利だが困るときもある

Go 1.13 からエラーがラップできるようになりました。これにより、下から持ち上がってきたエラーをラップしておけば、基本的にはエラーハンドリングで困ることはなくなりました。

しかし、エラーをラップしてしまうと、ラップしたエラーが全て Error 関数によって表示されてしまいます。

そのため、ユーザー向けのエラーを用意して、途中でエラーをキレイにしてから持ち上げ直す、みたいなことをする必要がある場合がありました(個人的には)。その場合、それまでのエラー情報が失われてしまうので、いったんそこまでのエラーをログに吐いてから、エラーを作り直す必要があるケースもありました。

    if err := Foo("some function returns internal error"); err != nil {
        log.Errorf("internal error: %v", err)
        return errors.New("user-facing error message)
    }

(趣味の問題はありますが)エラー吐くときに毎回ログするのは面倒ですし、サーバへのリクエストであれば、ミドルウエアに上がってきたエラーをまとめて処理できたら便利なのに・・・みたいなこともあるでしょう。しかし、ユーザー向けのエラーを用意してしまうと、エラーはユーザー向けのキレイな何の情報もないエラーになってしまっていて、役に立ちません。

ユーザー向けのエラーメッセージに隠蔽しつつエラーの情報を落とさない

そこで、エラーをラップしつつ、ラップしたエラーは Error によっては表示せず、errors.Is / errors.As では取り出せるようにする仕組みを用意しました。

エラーをオブラートに包んで上に送り出します。

func Example() {
    e1 := errors.New("e1")
    e2 := errors.New("e2")
    e3 := fmt.Errorf("wrapped error: %w", io.EOF)

    // New returns an error that wraps the given errors.
    err := oblate.New("user-facing error message", e1, e2, e3)

    // The error formats as the strings obtained by calling the Error
    // method of the first error in errs.
    fmt.Println("error:")
    fmt.Println(err.Error())

    // Cause returns the error formats as the concatenation of the strings
    // obtained by calling the Error method of each element of errs
    // except the first error, with a newline between each string.
    fmt.Println("cause:")
    fmt.Println(err.(*oblate.Error).Cause())

    // The error wrapped by oblate can be checked with errors.Is and errors.As.
    fmt.Println("error details:")
    fmt.Printf("errors.Is(err, io.EOF): %v\n", errors.Is(err, io.EOF))

    // Output:
    // error:
    // user-facing error message
    // cause:
    // e1
    // e2
    // EOF
    // error details:
    // errors.Is(err, io.EOF): true
}

エラーメッセージはユーザー向けになりますが、内部情報は取り出せます。

仕組み

Go の標準で用意されている errors.Join は、エラーを複数の連結できます。errors.Join で連結したエラーの Error は連結したエラーを改行で連結した文字列を返します。たとえば、エラー e1, e2, e3 を連結したら

e1
e2
e3

となります。Oblate は、errors.Join と同じ仕組みですが、一番最初のエラーだけを Error で返します。実際、Oblate には oblate.Join という関数が用意されていて、oblate.New("hello", e1, e2)oblate.Join(errors.New("hello"), e1, e2) と同じです。

これによって、エラーメッセージをユーザー向けに変更しつつ、これまでラップしてきたエラーの情報を落とさないようにしています。

まとめ

ユーザーがいるときは、エラーを返す前にオブラートに包んであげてください。苦い思いをしなくて済むかも知れません。

ちなみに、Goa のエラーは、内部にエラーを持ちつつ、エラーメッセージを作るので、ミドルウエアでエラーをログに出す、みたいなことがそのままできます。@tchssk san がこれを便利に出来るすばらしいプラグインを作成されています。ご参考まで。

https://tchssk.hatenablog.com/entry/2023/12/26/161614

Happy hacking!

Discussion