Consider better error handling for web apps

Consider better error handling for web apps

9eed44f137609e6ce3b6f1e14f80b9e1?s=128

Masayuki Izumi

November 05, 2017
Tweet

Transcript

  1. Consider better error handling for web apps Go Conference 2017

    Autumn - by @izumin5210
  2. How do you handle errors in applications ?

  3. Ignore ? ``` resp, _ := DoSomething() ```

  4. Panic ? ``` resp, err := DoSomething() if err !=

    nil { panic(err) } ```
  5. Wrap ? ``` import ( "github.com/pkg/errors" ) resp, err :=

    DoSomething() if err != nil { return errors.Wrap(err) } ```
  6. ### 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
  7. ### `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
  8. None
  9. Error reporting

  10. ### e.g. Error reporting as a Service - Honeybadger.io -

    Sentry etc. 10
  11. From where do we send error reports? 11

  12. #### 1. レスポンス直前 ``` if err != nil { notifyError(err)

    // <-- here! w.WriteHeader(500) return } // or middlewares, interceptors, etc. ``` - expected errorとunexpected errorをどう区別する? - status codeをどうやって決定する? 12
  13. #### 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
  14. ### 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
  15. ## 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
  16. ``` 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
  17. ``` 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
  18. ``` err := store.CreateWorkingHistory(userID, company) if err != nil {

    HandleError(w, err) return } ``` 18
  19. ### 独自Error typeはいいのか? Gocon Spring 2016 Keynote - APIは `github.com/pkg/errors`

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

    そのドメインでのエラーコード一覧をつくる? - エラー処理時にHTTP, gRPC等のStatus Codeへマップ 20
  21. Sample application izumin5210-sandbox/grpc-and-gateway-sample-app-go on github

  22. ### `ErrorCode` ``` // type/system/error.go type ErrorCode int const (

    ErrorUnknown ErrorCode = iota ErrorNotFound ErrorFailedToReadDB ) ``` 22
  23. ### 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
  24. ### 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
  25. ### Handle errors on interceptor ( `github.com/izumin5210/grpc-errors` : gRPCサーバ で

    `apperrors` をいい感じにやるやつ) 25
  26. #### 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
  27. ``` // 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
  28. #### 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
  29. #### Error report on sentry.io 29

  30. 30

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

    - `creasty/apperrors` - これをmiddlewareで処理する - これがベストとは思っていない - みなさんどうしてますか 31
  32. None
  33. ## 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