エラーをオブラートに包んでほしい
TL;DR;
- ユーザーに見せるエラーには内部の情報を含めたくない
- しかし、エラーに含まれている情報を落としたくはない
というときに、エラーをオブラートに包んで、ユーザー向けのエラーを用意しつつ、内部にはエラー情報を含んだままにします。
エラーの 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 がこれを便利に出来るすばらしいプラグインを作成されています。ご参考まで。
Happy hacking!
Discussion