Save 37% off PRO during our Black Friday Sale! »

エラー設計について / Designing Errors

エラー設計について / Designing Errors

アプリケーションのエラーハンドリングについて

- エラーとは?
- エラーに求められること
- Goのアプリケーションでエラーをどのように扱うか?

Failure is your Domainという記事を参考にしています。
https://middlemost.com/failure-is-your-domain/

Google Presentation版
https://docs.google.com/presentation/d/1JIdZ4IVW2D3kEFUtWSvHNes3r3ykojGuUAQAnhmEVs0/edit?usp=sharing

0f1d16574e6d15530b0e9e9b837d1d86?s=128

morikuni

May 18, 2019
Tweet

Transcript

  1. エラー設計について / Designing Errors Go Conference 2019 Spring 2019/05/18

  2. 自己紹介 • morikuni • https://twitter.com/inukirom • https://github.com/morikuni • Mercari /

    Microservices Development • Go & Application Architecture
  3. アプリケーションのエラーハンドリングについて • エラーとは? • エラーに求められること • Goのアプリケーションでエラーをどのように扱うか? Failure is your

    Domainという記事を参考にしています。 https://middlemost.com/failure-is-your-domain/ 本日の内容
  4. エラーとは?

  5. エラーとは? • エラーは処理が失敗したときに発生する • エラーには既知のエラーと未知のエラーの2種類がある ◦ 既知のエラー ▪ 発生することが想定できているエラー ▪

    例: 明示的にハンドリングされているエラー ◦ 未知のエラー ▪ 発生することが想定できていないエラー ▪ 例: panic, 500 Internal Server Errorになるようなエラー • 同じエラーでも状況によって既知か未知かは異なる
  6. エラーとは? どうやったら未知のエラーは既知になるのか? • エラーをハンドリングし、自分達のアプリケーションの一部として組み込む • 未知のエラーを既知とするための方法がエラーハンドリングである • Goはエラーを明示的に返却するので本当に未知なのはpanicくらい • 既知にもレベルがある

    ◦ エラーが発生することを想定していない (panic) ◦ エラーが発生することは知っているが詳細はわからない (error) ◦ 具体的にどういう種類のエラーが発生するのかを知っている (sql.ErrNoRowsなど)
  7. エラーに求められること

  8. エラーに求められること • エラーは、関係者に処理が失敗した原因を伝える必要がある • 次のような関係者がそれぞれ異なる情報を求めている ◦ アプリケーション ◦ エンドユーザー ◦

    運用者
  9. アプリケーション • エラーを識別できること(ハンドリングできること) • Goにおいては次のいずれかのパターンで表現される(と思う) エラーに求めること var ErrNotFound = errors.New("not

    found") type NotFound struct{} type Error struct { Code string } type NotFoundError interface { NotFound() }
  10. (クライアント)アプリケーション • エラーを識別できること(ハンドリングできること) • 多くの場合、通信プロトコルで定義されているエラー表現が使われる ◦ HTTP Status Code ◦

    gRPC Status Code エラーに求めること
  11. エンドユーザー • 問題を解決するヒントになるようなメッセージが含まれていること • メッセージはユーザーが理解できる形式であることが望ましい ◦ 例: お金を引き出そうとしたときに残高が足りなかった場合 ▪ Bad:

    「CODE 19283757 invalid amount」 ▪ Good: 「入力された金額が残高を上回っています。残高をご確認ください。」 エラーに求めること
  12. 運用者 • 問題の根本的な原因を調査、解決する手助けになる情報が含まれていること • エラーの識別情報やメッセージに加えてコールスタックやコンテキスト(引数など)も 表示し、エラーが発生した場所や状況が理解できるようになっていることが望ましい エラーに求めること

  13. ここまでのまとめ

  14. • エラーには既知のエラーと未知のエラーがある • 未知のエラーを既知とする方法がエラーハンドリングである • 関係者によってエラーに求める情報が異なる ◦ エラーの識別情報 (for アプリケーション)

    ◦ 人間が理解可能なメッセージ (for エンドユーザー) ◦ コールスタックやコンテキスト (for 運用者) ここまでのまとめ
  15. Goのアプリケーションで エラーをどのように扱うか?

  16. Goのアプリケーションでエラーをどのように扱うか? • アプリケーション固有のエラーコードを定義し、エラーを識別する ◦ HTTPとかgRPCもエラーコードによる識別をしているので従う • 既知の外部のエラーは上で定義したエラーコードに変換する • 定義したエラーコードのみを想定してエラーハンドリングをする ◦

    外部のエラーをそのまま使い回すと、想定すべきエラーが多くなり、アプリケーションのエ ラーハンドリングが複雑になる • エンドユーザーがエラーを解決できる場合にメッセージを追加する • 必要に応じてコールスタックや引数の情報を追加する • 未知のエラーが見つかったらハンドリングして既知にする
  17. github.com/morikuni/failure これらをアプリケーションで簡単に扱うために...

  18. • エラーコードを中心にしたエラーハンドリング • エラーメッセージ、コールスタック、コンテキスト情報の付与 • Wrapper interfaceによるカスタマイズ性の高さ • err.Error() の自動生成

    github.com/morikuni/failure
  19. failureを使ったエラーハンドリング

  20. github.com/morikuni/failure-example /simple-czrud • /create, /read, /update, /deleteの操作を持つ HTTPのKey-Value Store •

    Controller, Service, Database, Modelという シンプルな構成 • データベースにはMySQLを使用 failureを使ったエラーハンドリング
  21. • アプリケーション固有のエラーコードを定義する failureを使ったエラーハンドリング package errors import ( "github.com/morikuni/failure" ) const

    ( InvalidArgument failure.StringCode = "InvalidArgument" NotFound failure.StringCode = "NotFound" AlreadyExist failure.StringCode = "AlreadyExist" )
  22. • 既知の外部のエラーは定義したエラーコードに変換する ◦ failure.Translateでエラーにエラーコードを付与できる failureを使ったエラーハンドリング const query = ` SELECT

    v FROM kv WHERE k = ? ` r := db.conn.QueryRowContext(ctx, query, key) var i int64 if err := r.Scan(&i); err != nil { if err == sql.ErrNoRows { return 0, failure.Translate(err, errors.NotFound) } return 0, failure.Wrap(err) }
  23. • 定義したエラーコードのみを想定してエラーハンドリングをする ◦ failure.Isでエラーコードで分岐できる failureを使ったエラーハンドリング if !failure.Is(err, errors.NotFound) { return

    failure.Wrap(err, context, ) }
  24. • 定義したエラーコードのみを想定してエラーハンドリングをする ◦ failure.CodeOfでエラーコードを取り出せる failureを使ったエラーハンドリング func httpStatus(err error) int {

    switch c, _ := failure.CodeOf(err); c { case errors.InvalidArgument: return http.StatusBadRequest case errors.NotFound: return http.StatusNotFound case errors.AlreadyExist: return http.StatusConflict default: return http.StatusInternalServerError } }
  25. • エンドユーザーがエラーを解決できる場合にメッセージを追加する ◦ failure.Messageをfailure.Newやfailure.Translateのオプションとして渡せる failureを使ったエラーハンドリング _, err := s.db.Get(ctx, key)

    if err == nil { return failure.New(errors.AlreadyExist, failure.Message("Specified key already exists. Use update for existing key."), ) }
  26. • エンドユーザーがエラーを解決できる場合にメッセージを追加する ◦ failure.MessageOfで付与したメッセージのみを取り出せる failureを使ったエラーハンドリング msg, ok := failure.MessageOf(err) if

    ok { io.WriteString(w, msg) } else { io.WriteString(w, http.StatusText(status)) }
  27. • 必要に応じてコールスタックや引数の情報を追加する ◦ failure.Newやfailure.Wrapを使うと自動でコールスタックが追加される failureを使ったエラーハンドリング func (s *service) Create(ctx context.Context,

    key model.Key, value model.Value) error { context := failure.Context{"key": string(key)} ... if err != nil { return failure.Wrap(err, context, ) } return nil }
  28. • 必要に応じてコールスタックや引数の情報を追加する ◦ err.Error()などが自動で生成される failureを使ったエラーハンドリング c.logger.Printf("%v\n", err) // controller.(*Controller).create: service.(*service).Create:

    Specified key already exists. Use update for existing key.: key=a: code(AlreadyExist) c.logger.Printf("%+v\n", err) // [controller.(*Controller).create] /Users/morikuni/go/src/github.com/morikuni/failure-example/simple-crud/controller/controller.go:76 // [service.(*service).Create] /Users/morikuni/go/src/github.com/morikuni/failure-example/simple-crud/service/service.go:32 // message("Specified key already exists. Use update for existing key.") // key = a // code(AlreadyExist) // [CallStack] // [service.(*service).Create] /Users/morikuni/go/src/github.com/morikuni/failure-example/simple-crud/service/service.go:32 // [controller.(*Controller).create] /Users/morikuni/go/src/github.com/morikuni/failure-example/simple-crud/controller/controller.go:74 // …(略)
  29. • 未知のエラーが見つかったらハンドリングして既知にする ◦ 運用をしているとエラーコードやメッセージが付いていないエラーが見つかる ▪ failure.CodeOfやfailure.MessageOfの第2返り値がfalseになっているエラー ◦ これらのエラーが未知のエラーである ◦ 未知のエラーが見つかったら、エラーハンドリングをしてエラーコードやメッセージを付与

    し、未知のエラーを既知のものとしていく failureを使ったエラーハンドリング
  30. failureとxerrorsの考え方の違い

  31. failureとxerrorsの考え方の違い • failureはerror型にOptionalフィールドを足してエラーを拡張するイメージ ◦ 1つのWrapperが1つのフィールドや機能を追加する • xerrorsはエラーを変換していくイメージ type Error interface

    { Error() string ErrorCode() (Code, bool) CallStack() (CallStack, bool) Message() (string, bool) // other optional fields... } AppError{ URLError{ SyscallError{ Errno() } } }
  32. failureとxerrorsの考え方の違い failure xerrors エラーの比較 failure.Isによって最新のエラー コードを見る xerrors.Isによってエラーが含まれてい るか見る エラー情報の取り出し failure.MessageOfのように1

    フィールド毎に関数を用意する xerrors.Asによってオブジェクトに情報 をマッピングする エラーのラップ方法 failure.Wrapper interfaceでラッピ ング方法が統一されている ラッパー毎にxerrors.Errorfなどの専用 の関数を用意する カスタマイズ性 Wrapperは1つずつ独立して使用 可能 挙動を変えるためには、新しい型を実 装する
  33. failureとxerrorsの考え方の違い failureとxerrorsのどちらを使えばいいのか? • アプリケーション(エンドユーザーに機能を提供する)ものであれば、failure • ライブラリ(アプリケーションに組み込まれる)ものであれば、xerrors ライブラリでfailureを使うと、ライブラリのエラー情報とfailureを使うアプリケーションのエ ラー情報が混ざってしまって区別するのが困難になる (例: ライブラリのMessageがアプリケーションのエンドユーザーに出てしまう)

  34. まとめ

  35. • エラーには既知のエラーと未知のエラーがある • 未知のエラーを既知とする方法がエラーハンドリングである • 関係者によってエラーに求める情報が異なる ◦ エラーの識別情報 (for アプリケーション)

    ◦ 人間が理解可能なメッセージ (for エンドユーザー) ◦ コールスタックやコンテキスト (for 運用者) • アプリケーション内のエラーを定義し、自分達でエラーを管理しよう • github.com/morikuni/failure オススメです まとめ