...
1
2# How do we handle error?
3In Go, the general approach is that every func should return an error so we can handle it immediatly.
4That is not the case when it comes to HTTP handlers.
5
6```
7func(http.ResponseWriter, *http.Request)
8```
9
10That cause our code to have logs all over the handler code since each error is logged and handled.
11
12### Can we do better?
13What if our HTTP handler returned an error?
14```
15func(http.ResponseWriter, *http.Request) error
16```
17
18In that case,
19Error handling will be delegated to a centric place, where we can do all logging too.
20
21
22### How should our handler act upon error?
23All inner calls should return an error to our handler.
24Upon an error, our handler should decide how to act.
25
26We have mapped the current possible acts:
27- abort with error
28- abort with status
29- abort with status JSON
30- redirect
31
32We will create a dedicated error for each possible handler act.
33Upon error, the handler will return one of those:
34
35```
36type AbortError struct {
37 Code int
38 Err error
39}
40
41type StatusError struct {
42 Code int
43 Err error
44}
45
46type JSONError struct {
47 Code int
48 Message string
49 Details map[string]interface{}
50 Err error
51}
52
53type RedirectError struct {
54 Code int
55 Location string
56 Err error
57}
58```
59
60A typical handler, can call other packages like a db pkg.
61In case the other package code has an error we can decide to:
62- simply return the error back to the handler,
63- wrap the error back to the handler
64- wrap it into a custom `AppError`
65
66For the 3rd option,
67We will create a custom `AppError` error that would be used by the packages that the handler call to.
68```
69type AppError struct {
70 code string
71 message string
72 err error
73}
74```
75
76### Example
77
78```
79func TestJSONError(t *testing.T) {
80 // simulate a db error
81 dbErr := errors.New("SQL_ERR::NO_ROWS")
82
83 // handler wraps the db error and constructs a JSONError...
84 handlerErr := apperror.JSONError{Code: 401, Err: dbErr, Message: "user not found", Details: gin.H{"k1": "v1", "k2": 2}}
85
86 assert.True(t, apperror.IsJSONResponder(handlerErr))
87 assert.Equal(t, map[string]interface{}{"k1": "v1", "k2": 2}, handlerErr.JSONDetails())
88 assert.Equal(t, "user not found. SQL_ERR::NO_ROWS", handlerErr.Error())
89}
90```
91
92Since the handler returns a JSONError (which implements the `JSONResponder` interface),
93Our handler wrapper will now handle ths error:
94
95```
96func MakeHandlerFunc(f types.APIFunc) gin.HandlerFunc {
97 return func(c *gin.Context) {
98 if err := f(c); err != nil {
99 log := log.Get(c.Request.Context())
100 .
101 .
102 .
103 if jsonErr, ok := err.(apperror.JSONResponder); ok {
104 //msg := fmt.Sprintf("[%v] - %v", ShortOperationID(c.Request.Context()), apperror.ErrorChain(jsonErr))
105 code, jsonObj := jsonErr.JSONResponse()
106 msg := fmt.Sprintf("[%v] - (%d) aborting with json", ShortOperationID(c.Request.Context()), code)
107 log.Error(jsonErr, msg, "details", jsonErr.JSONDetails())
108
109 c.AbortWithStatusJSON(code, jsonObj)
110 return
111 }
112 .
113 .
114 .
115 // handle non AppError
116 log.Error(err, "unexpected error occurred")
117 c.AbortWithError(500, errors.New("unexpected error occurred")) //nolint:errcheck
118 return
119 }
120 }
121}
122
123
124```
View as plain text