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

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. Consider better error handling for web apps
    Go Conference 2017 Autumn - by @izumin5210

    View Slide

  2. How do you handle errors in applications ?

    View Slide

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

    View Slide

  4. Panic ?
    ```
    resp, err := DoSomething()
    if err != nil {
    panic(err)
    }
    ```

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  8. View Slide

  9. Error reporting

    View Slide

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

    View Slide

  11. From where do we send error reports?
    11

    View Slide

  12. #### 1. レスポンス直前
    ```
    if err != nil {
    notifyError(err) // <-- here!
    w.WriteHeader(500)
    return
    }
    // or middlewares, interceptors, etc.
    ```
    - expected errorとunexpected errorをどう区別する?
    - status codeをどうやって決定する?
    12

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  18. ```
    err := store.CreateWorkingHistory(userID, company)
    if err != nil {
    HandleError(w, err)
    return
    }
    ```
    18

    View Slide

  19. ### 独自Error typeはいいのか?
    Gocon Spring 2016 Keynote
    - APIは `github.com/pkg/errors` + α
    - `WithReport()` と `WithStatusCode()` ぐらい
    - errorをmiddlewareで処理すれば依存は最小限?
    19

    View Slide

  20. ### `WithStatusCode` をDBに近いレイヤーで使うの?
    - これはあまりうれしくない
    - HTTP Status CodeはclientへのViewの一つでしかない
    - そのドメインでのエラーコード一覧をつくる?
    - エラー処理時にHTTP, gRPC等のStatus Codeへマップ
    20

    View Slide

  21. Sample application
    izumin5210-sandbox/grpc-and-gateway-sample-app-go
    on github

    View Slide

  22. ### `ErrorCode`
    ```
    // type/system/error.go
    type ErrorCode int
    const (
    ErrorUnknown ErrorCode = iota
    ErrorNotFound
    ErrorFailedToReadDB
    )
    ```
    22

    View Slide

  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

    View Slide

  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

    View Slide

  25. ### Handle errors on interceptor
    ( `github.com/izumin5210/grpc-errors` : gRPCサーバ
    で `apperrors` をいい感じにやるやつ)
    25

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  29. #### Error report on sentry.io
    29

    View Slide

  30. 30

    View Slide

  31. ## Conclusion
    - アプリケーション改善には `pkg/errors` じゃ足りない?
    - error reportやstatus codeをうまく扱いたい
    - `creasty/apperrors`
    - これをmiddlewareで処理する
    - これがベストとは思っていない
    - みなさんどうしてますか
    31

    View Slide

  32. View Slide

  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

    View Slide