$30 off During Our Annual Pro Sale. View Details »

エラー設計について / 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

morikuni

May 18, 2019
Tweet

More Decks by morikuni

Other Decks in Technology

Transcript

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

    View Slide

  2. 自己紹介
    ● morikuni
    ● https://twitter.com/inukirom
    ● https://github.com/morikuni
    ● Mercari / Microservices Development
    ● Go & Application Architecture

    View Slide

  3. アプリケーションのエラーハンドリングについて
    ● エラーとは?
    ● エラーに求められること
    ● Goのアプリケーションでエラーをどのように扱うか?
    Failure is your Domainという記事を参考にしています。
    https://middlemost.com/failure-is-your-domain/
    本日の内容

    View Slide

  4. エラーとは?

    View Slide

  5. エラーとは?
    ● エラーは処理が失敗したときに発生する
    ● エラーには既知のエラーと未知のエラーの2種類がある
    ○ 既知のエラー
    ■ 発生することが想定できているエラー
    ■ 例: 明示的にハンドリングされているエラー
    ○ 未知のエラー
    ■ 発生することが想定できていないエラー
    ■ 例: panic, 500 Internal Server Errorになるようなエラー
    ● 同じエラーでも状況によって既知か未知かは異なる

    View Slide

  6. エラーとは?
    どうやったら未知のエラーは既知になるのか?
    ● エラーをハンドリングし、自分達のアプリケーションの一部として組み込む
    ● 未知のエラーを既知とするための方法がエラーハンドリングである
    ● Goはエラーを明示的に返却するので本当に未知なのはpanicくらい
    ● 既知にもレベルがある
    ○ エラーが発生することを想定していない (panic)
    ○ エラーが発生することは知っているが詳細はわからない (error)
    ○ 具体的にどういう種類のエラーが発生するのかを知っている (sql.ErrNoRowsなど)

    View Slide

  7. エラーに求められること

    View Slide

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

    View Slide

  9. アプリケーション
    ● エラーを識別できること(ハンドリングできること)
    ● Goにおいては次のいずれかのパターンで表現される(と思う)
    エラーに求めること
    var ErrNotFound = errors.New("not found") type NotFound struct{}
    type Error struct {
    Code string
    }
    type NotFoundError interface {
    NotFound()
    }

    View Slide

  10. (クライアント)アプリケーション
    ● エラーを識別できること(ハンドリングできること)
    ● 多くの場合、通信プロトコルで定義されているエラー表現が使われる
    ○ HTTP Status Code
    ○ gRPC Status Code
    エラーに求めること

    View Slide

  11. エンドユーザー
    ● 問題を解決するヒントになるようなメッセージが含まれていること
    ● メッセージはユーザーが理解できる形式であることが望ましい
    ○ 例: お金を引き出そうとしたときに残高が足りなかった場合
    ■ Bad: 「CODE 19283757 invalid amount」
    ■ Good: 「入力された金額が残高を上回っています。残高をご確認ください。」
    エラーに求めること

    View Slide

  12. 運用者
    ● 問題の根本的な原因を調査、解決する手助けになる情報が含まれていること
    ● エラーの識別情報やメッセージに加えてコールスタックやコンテキスト(引数など)も
    表示し、エラーが発生した場所や状況が理解できるようになっていることが望ましい
    エラーに求めること

    View Slide

  13. ここまでのまとめ

    View Slide

  14. ● エラーには既知のエラーと未知のエラーがある
    ● 未知のエラーを既知とする方法がエラーハンドリングである
    ● 関係者によってエラーに求める情報が異なる
    ○ エラーの識別情報 (for アプリケーション)
    ○ 人間が理解可能なメッセージ (for エンドユーザー)
    ○ コールスタックやコンテキスト (for 運用者)
    ここまでのまとめ

    View Slide

  15. Goのアプリケーションで
    エラーをどのように扱うか?

    View Slide

  16. Goのアプリケーションでエラーをどのように扱うか?
    ● アプリケーション固有のエラーコードを定義し、エラーを識別する
    ○ HTTPとかgRPCもエラーコードによる識別をしているので従う
    ● 既知の外部のエラーは上で定義したエラーコードに変換する
    ● 定義したエラーコードのみを想定してエラーハンドリングをする
    ○ 外部のエラーをそのまま使い回すと、想定すべきエラーが多くなり、アプリケーションのエ
    ラーハンドリングが複雑になる
    ● エンドユーザーがエラーを解決できる場合にメッセージを追加する
    ● 必要に応じてコールスタックや引数の情報を追加する
    ● 未知のエラーが見つかったらハンドリングして既知にする

    View Slide

  17. github.com/morikuni/failure
    これらをアプリケーションで簡単に扱うために...

    View Slide

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

    View Slide

  19. failureを使ったエラーハンドリング

    View Slide

  20. github.com/morikuni/failure-example
    /simple-czrud
    ● /create, /read, /update, /deleteの操作を持つ
    HTTPのKey-Value Store
    ● Controller, Service, Database, Modelという
    シンプルな構成
    ● データベースにはMySQLを使用
    failureを使ったエラーハンドリング

    View Slide

  21. ● アプリケーション固有のエラーコードを定義する
    failureを使ったエラーハンドリング
    package errors
    import (
    "github.com/morikuni/failure"
    )
    const (
    InvalidArgument failure.StringCode = "InvalidArgument"
    NotFound failure.StringCode = "NotFound"
    AlreadyExist failure.StringCode = "AlreadyExist"
    )

    View Slide

  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)
    }

    View Slide

  23. ● 定義したエラーコードのみを想定してエラーハンドリングをする
    ○ failure.Isでエラーコードで分岐できる
    failureを使ったエラーハンドリング
    if !failure.Is(err, errors.NotFound) {
    return failure.Wrap(err,
    context,
    )
    }

    View Slide

  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
    }
    }

    View Slide

  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."),
    )
    }

    View Slide

  26. ● エンドユーザーがエラーを解決できる場合にメッセージを追加する
    ○ failure.MessageOfで付与したメッセージのみを取り出せる
    failureを使ったエラーハンドリング
    msg, ok := failure.MessageOf(err)
    if ok {
    io.WriteString(w, msg)
    } else {
    io.WriteString(w, http.StatusText(status))
    }

    View Slide

  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
    }

    View Slide

  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
    // …(略)

    View Slide

  29. ● 未知のエラーが見つかったらハンドリングして既知にする
    ○ 運用をしているとエラーコードやメッセージが付いていないエラーが見つかる
    ■ failure.CodeOfやfailure.MessageOfの第2返り値がfalseになっているエラー
    ○ これらのエラーが未知のエラーである
    ○ 未知のエラーが見つかったら、エラーハンドリングをしてエラーコードやメッセージを付与
    し、未知のエラーを既知のものとしていく
    failureを使ったエラーハンドリング

    View Slide

  30. failureとxerrorsの考え方の違い

    View Slide

  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()
    }
    }
    }

    View Slide

  32. failureとxerrorsの考え方の違い
    failure xerrors
    エラーの比較
    failure.Isによって最新のエラー
    コードを見る
    xerrors.Isによってエラーが含まれてい
    るか見る
    エラー情報の取り出し
    failure.MessageOfのように1
    フィールド毎に関数を用意する
    xerrors.Asによってオブジェクトに情報
    をマッピングする
    エラーのラップ方法
    failure.Wrapper interfaceでラッピ
    ング方法が統一されている
    ラッパー毎にxerrors.Errorfなどの専用
    の関数を用意する
    カスタマイズ性
    Wrapperは1つずつ独立して使用
    可能
    挙動を変えるためには、新しい型を実
    装する

    View Slide

  33. failureとxerrorsの考え方の違い
    failureとxerrorsのどちらを使えばいいのか?
    ● アプリケーション(エンドユーザーに機能を提供する)ものであれば、failure
    ● ライブラリ(アプリケーションに組み込まれる)ものであれば、xerrors
    ライブラリでfailureを使うと、ライブラリのエラー情報とfailureを使うアプリケーションのエ
    ラー情報が混ざってしまって区別するのが困難になる
    (例: ライブラリのMessageがアプリケーションのエンドユーザーに出てしまう)

    View Slide

  34. まとめ

    View Slide

  35. ● エラーには既知のエラーと未知のエラーがある
    ● 未知のエラーを既知とする方法がエラーハンドリングである
    ● 関係者によってエラーに求める情報が異なる
    ○ エラーの識別情報 (for アプリケーション)
    ○ 人間が理解可能なメッセージ (for エンドユーザー)
    ○ コールスタックやコンテキスト (for 運用者)
    ● アプリケーション内のエラーを定義し、自分達でエラーを管理しよう
    ● github.com/morikuni/failure オススメです
    まとめ

    View Slide