Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Consider better error handling for web apps

Consider better error handling for web apps

Masayuki Izumi

November 05, 2017
Tweet

More Decks by Masayuki Izumi

Other Decks in Programming

Transcript

  1. Wrap ? ``` import ( "github.com/pkg/errors" ) resp, err :=

    DoSomething() if err != nil { return errors.Wrap(err) } ```
  2. ### Why should we `Wrap` errors ? The errors package

    allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error. github.com/pkg/errors (emphasis mine) 6
  3. ### `return nil, errors.Wrap(err)` , and what then? ``` func

    GetProfile(w http.ResponseWriter, r *http.Request) { // ... s, err := store.GetProfileByID(userID) if err != nil { w.WriteHeader(500) return } // ... } ``` 7
  4. #### 1. レスポンス直前 ``` if err != nil { notifyError(err)

    // <-- here! w.WriteHeader(500) return } // or middlewares, interceptors, etc. ``` - expected errorとunexpected errorをどう区別する? - status codeをどうやって決定する? 12
  5. #### 2. エラー出次第? ``` q := "INSERT INTO working_histories (profile_id,

    company) VALUES ($1, $2)" result, err := conn.Exec(q, 1253477, "Wantedly, Inc.") if err != nil { errorreporter.NotifyError(err) // <-- here! return nil, errors.Wrap(err, "failed to write new working history") } ``` - あらゆるレイヤがerror reporterと密結合することになる - 「どこで `NotifyError` したか」わからなくなる 13
  6. ### For appropriate error reporting - Adds context to errors

    - It's provided by `github.com/pkg/errors` - Decides whether errors is as expected or unexpected - Decides responses to users (e.g. HTTP Status Code, etc.) 14
  7. ## Better error annotations for applications e.g. `github.com/creasty/apperrors` `apperrors` provides

    contextual metadata to errors. - Stack trace - Additional information - Status code (for a HTTP server) - Reportability (for an integration with error reporting service) 15
  8. ``` if isProfileExists(profileID) { // can annotate errors with error

    codes return nil, apperrors.WithStatusCode( apperrors.New("profile does not exist"), http.StatusNotFound, ) } result, err := conn.Exec("INSERT INTO ...", profileID, company) if err != nil { // can annotate unexpected errors return nil, apperrors.WithReport( apperrors.WithMessage(err, "failed to write new working history"), ) } ``` 16
  9. ``` func HandleError(w http.ResponseWriter, err error) { appErr := apperrors.Unwrap(err)

    if appErr.Report { // We can send report only about unexpected errors go uploadAppError(appErr) } if appErr.StatusCode > 0 { w.WriteHeader(appErr.StatusCode) } else { w.WriteHeader(http.StatusInternalServerError) } } ``` 17
  10. ### 独自Error typeはいいのか? Gocon Spring 2016 Keynote - APIは `github.com/pkg/errors`

    + α - `WithReport()` と `WithStatusCode()` ぐらい - errorをmiddlewareで処理すれば依存は最小限? 19
  11. ### `WithStatusCode` をDBに近いレイヤーで使うの? - これはあまりうれしくない - HTTP Status CodeはclientへのViewの一つでしかない -

    そのドメインでのエラーコード一覧をつくる? - エラー処理時にHTTP, gRPC等のStatus Codeへマップ 20
  12. ### `ErrorCode` ``` // type/system/error.go type ErrorCode int const (

    ErrorUnknown ErrorCode = iota ErrorNotFound ErrorFailedToReadDB ) ``` 22
  13. ### Functions for `ErrorCode` ``` // type/system/error.go func (c ErrorCode)

    Wrap(err error) error { return apperrors.WithStatusCode(apperrors.Wrap(err), int(c)) } func (c ErrorCode) WithReport(err error) error { return apperrors.WithReport(c.Wrap(err)) } ``` 23
  14. ### Annotate errors ``` // store/profile/store.go err := s.DB.Get(prof, "SELECT

    * FROM profiles WHERE user_id = $1", userID) if err != nil { if err == sql.ErrNoRows { err = errors.Wrap(err, "profile was not found") return nil, system.ErrorNotFound.Wrap(err) } err = errors.Wrap(err, "failed to read profile") return nil, system.ErrorFailedToReadDB.WithReport(err) } ``` 24
  15. #### Convert status code ``` // store/interceptor/errors.go var grpcCodeBySystemCode =

    map[system.ErrorCode]codes.Code{ system.ErrorUnknown: codes.Unknown, system.ErrorNotFound: codes.NotFound, system.ErrorFailedToReadDB: codes.Internal, } ``` 26
  16. ``` // store/interceptor/errors.go grpcerrors.WithStatusCodeMapper(func(code int) codes.Code { grpcCode, ok :=

    grpcCodeBySystemCode[system.ErrorCode(code)] if !ok { return codes.Unknown } return grpcCode }) ``` 27
  17. #### Send error reports ``` // store/interceptor/errors.go grpcerrors.WithUnaryServerReportableErrorHandler(func(...) error {

    st := &raven.Stacktrace{} for _, t := range err.StackTrace { // create and append stacktrace frames } pckt := raven.NewPacket(err.Error(), st) // set contextual metadata to packet raven.Capture(pckt, map[string]string{"method": info.FullMethod}) return err }), ``` 28
  18. 30

  19. ## Conclusion - アプリケーション改善には `pkg/errors` じゃ足りない? - error reportやstatus codeをうまく扱いたい

    - `creasty/apperrors` - これをmiddlewareで処理する - これがベストとは思っていない - みなさんどうしてますか 31
  20. ## References - articles - Gocon Spring 2016 Keynote -

    Golangのエラー処理とpkg/errors | SOTA - packages - github.com/pkg/errors - github.com/creasty/apperrors - github.com/izumii5210/grpc-errors - Sample application - izumin5210-sandbox/grpc-and-gateway-sample-app-go 33