Failure is your domain. Effective error handling in any GoLang package or application is crucial to understand how and where errors are formulated. Errors used in combination with effective logging can save hours of debugging time.
See the supporting repository - github.com/ainsleyclark/errors
Why should we err?
Errors are as important in your domain and application as entities such as a User or Database. They should be treated as individual types. Not only do they give a clear meaning to the users of the application if something goes wrong, but they can save hours of debugging time when used effectively.
Coupled with consistent and effective use of a logging package, we are able to tell if something goes wrong, where it went wrong and how it went wrong.
Go errors
I think we are blessed with the nature of Go’s simplistic style of error handling. It’s certainly caught the attention of users of other programming languages. All we have to do is check if the error is not nil, and that’s it.
1file, err := os.Open("gopher.jpg")
2if err != nil {
3 return err
4}
Coming from Java or PHP you may be wondering where the try
/catch
/finally
blocks are. But with this simple idiom it
makes for smaller functions and having no throws flags shortens method signatures.
What is an error?
An error is anything that implements the singular method interface error
in the standard library interface. An
error _type _ must implement a method that returns a string.
1// The error built-in interface type is the conventional interface for
2// representing an error condition, with the nil value representing no error.
3type error interface {
4 Error() string
5}
Digging deeper into the stdlib library, there is only one implementation of this interface called errorString
, which
gets constructed when errors.New()
is called.
1// errorString is a trivial implementation of error.
2type errorString struct {
3 s string
4}
This simply returns s
when .Error()
is called. Indeed fmt.Errorf
also constructs an errorString
when called
with additional formatting.
Implementing custom errors
What the authors of Go are saying to you here is that they want you to implement the error
interface, and by doing so,
you can transform any type into an error of your own.
1type CustomError struct {
2 Err error
3}
4
5func (c *CustomError) Error() string {
6 if c.Err != nil {
7 return c.Err.Error()
8 }
9 return "An error has occurred"
10}
Here we are creating a CustomError
type and implementing the single method interface Error()
which means we can now
return an error
. To use it, we just simply return a pointer to CustomError
.
1func SomethingBroken() error {
2 // Something went wrong...
3 return &CustomError{
4 Err: errors.New("broken"),
5 }
6}
Digging deeper
This is an ok approach, but we haven’t improved, much if at all on the standard errorString
. Taking a look
at
Ben Johnson’s approach we can do a lot better in terms of being
more verbose. How can we tell where the error occurred in our application? There is also no formatted message for the
end user. What do they care about application error messages?
1// Error defines a standard application error.
2type Error struct {
3 // The application error code.
4 Code string `json:"code"`
5 // A human-readable message to send back to the end user.
6 Message string `json:"message"`
7 // Defines what operation is currently being run.
8 Op string `json:"operation"`
9 // The error that was returned from the caller.
10 Err error `json:"error"`
11}
Enter our new Error
type, already looking better. Here we are explicitly defining the code, message and operation of
the errors that occur in application.
Implementing Error()
To make our error type conform with the standard library interface, we need to implement Error()
. Below we are
implementing the Error
interface and returning a formatted error to a byte buffer.
- Print the code if there is one attached.
- Print the file line and operation, if any.
- If there is an error, print the wrapping Error().
- Continue to print the message and clean up.
1// Error returns the string representation of the error
2// message by implementing the error interface.
3func (e *Error) Error() string {
4 var buf bytes.Buffer
5
6 // Print the error code if there is one.
7 if e.Code != "" {
8 buf.WriteString("<" + e.Code + "> ")
9 }
10
11 // Print the file-line, if any.
12 if e.fileLine != "" {
13 buf.WriteString(e.fileLine + " - ")
14 }
15
16 // Print the current operation in our stack, if any.
17 if e.Operation != "" {
18 buf.WriteString(e.Operation + ": ")
19 }
20
21 // Print the original error message, if any.
22 if e.Err != nil {
23 buf.WriteString(e.Err.Error() + ", ")
24 }
25
26 // Print the message, if any.
27 if e.Message != "" {
28 buf.WriteString(e.Message)
29 }
30
31 return strings.TrimSuffix(strings.TrimSpace(buf.String()), ",")
32}
This will print something similar to below:
1<internal> /Users/me/project/store/users.go:27 - UserStore.Find: syntax error near SELECT, Error executing SQL query
Constructors
An idiomatic way for developers to create the Error type would be to create a variety of utility functions that allows for the creation of errors by code. Below are a few examples taken from github.com/ainsleyclark/errors.
1// NewInternal returns an Error with a INTERNAL error code.
2func NewInternal(err error, message, op string) *Error {
3 return newError(err, message, INTERNAL, op)
4}
5
6// NewConflict returns an Error with a CONFLICT error code.
7func NewConflict(err error, message, op string) *Error {
8 return newError(err, message, CONFLICT, op)
9}
10
11// NewInvalid returns an Error with a INVALID error code.
12func NewInvalid(err error, message, op string) *Error {
13 return newError(err, message, INVALID, op)
14}
The newError
utility function constructs a new *Error
with the file, line and program counters to obtain stack
traces from the error.
1// newError is an alias for New by creating the pcs
2// file line and constructing the error message.
3func newError(err error, message, code, op string) *Error {
4 _, file, line, _ := runtime.Caller(2)
5 pcs := make([]uintptr, 100)
6 _ = runtime.Callers(3, pcs)
7 return &Error{
8 Code: code,
9 Message: message,
10 Operation: op,
11 Err: err,
12 fileLine: file + ":" + strconv.Itoa(line),
13 pcs: pcs,
14 }
15}
Codes
The API needs to determine what type of error occurred. For example, if a http request was made to an external service,
this could be an INTERNAL
error. Similarly if an entity wasn’t found from a datastore, it should return a NOT_FOUND
error.
The error code could of course be an int or a string depending on your needs. We could get even fancier here and
abstract Code
to be a custom type, with a map of what each error code describes. But for now we can establish error
codes to something similar to below.
1const (
2 // CONFLICT - An action cannot be performed.
3 CONFLICT = "conflict"
4 // INTERNAL - Error within the application
5 INTERNAL = "internal"
6 // INVALID - Validation failed
7 INVALID = "invalid"
8 // NOTFOUND - Entity does not exist
9 NOTFOUND = "not_found"
10 // TEMPLATE - Templating error
11 TEMPLATE = "template"
12)
As the interface Error
only returns a string, we need a way to establish what error code it is, if any. The function
below takes in any error and retrieves its relevant code.
- Returns an empty string for
nil
errors. - Searches the chain of
Error.Err
until a code is found. - Return
INTERNAL
if no code is found.
1// Code returns the code of the root error, if available.
2// Otherwise returns INTERNAL.
3func Code(err error) string {
4 if err == nil {
5 return ""
6 } else if e, ok := err.(*Error); ok && e.Code != "" {
7 return e.Code
8 } else if ok && e.Err != nil {
9 return Code(e.Err)
10 }
11 return INTERNAL
12}
HTTP & gRPC
This methodology lends itself greatly to generic systems that define codes in a simple way to reason about, such as HTTP & gRPC.
Transforming the error codes into HTTP & gRPC codes means we can easily send the correct response codes to different
services. The function below returns an int
representing a HTTP status code, defaulting
to http.StatusInternalServerError
.
1// HTTPStatusCode is a convenience method used to get the appropriate
2// HTTP response status code for the respective error type.
3func (e *Error) HTTPStatusCode() int {
4 status := http.StatusInternalServerError
5 switch e.Code {
6 case CONFLICT:
7 return http.StatusConflict
8 case INVALID:
9 return http.StatusBadRequest
10 case NOTFOUND:
11 return http.StatusNotFound
12 case EXPIRED:
13 return http.StatusPaymentRequired
14 case MAXIMUMATTEMPTS:
15 return http.StatusTooManyRequests
16 }
17 return status
18}
Handlers
Now we utilise this extra functionality in our handlers, by sending the correct HTTP response according to the error code we instantiated in our service or datastore.
1func (h *Handler) Find() http.HandlerFunc {
2 return func(w http.ResponseWriter, r *http.Request) {
3 id := chi.URLParam(r, "id")
4
5 item, err := h.service.Find(r.Context(), id)
6 if err != nil {
7 api.Error(r, errors.ToError(err).HTTPStatusCode(), err)
8 return
9 }
10
11 api.Respond(r, 200, item, "Successfully obtained User with ID: "+ id)
12 }
13}
Messages
If you are an end user digesting an application, a user friendly error message is critical to include to any
application. 9 times out 10, the end user does not have any developer experience, and receiving something similar
to: SQL error near JOIN
won’t make any sense.
When a new Error
is created, we can attach a user-friendly message. However we need a utility function to extract
messages from error
values.
- Returns an empty string for
nil
errors. - Searches the chain of
Error.Err
until a code is found. - Returns a predefined error message in the case that no message is found.
1// GlobalError is a general message when no error message
2// has been found.
3const GlobalError = "An error has occurred."
4
5// Message returns the human-readable message of the error,
6// if available. Otherwise returns a generic error
7// message.
8func Message(err error) string {
9 if err == nil {
10 return ""
11 } else if e, ok := err.(*Error); ok && e.Message != "" {
12 return e.Message
13 } else if ok && e.Err != nil {
14 return Message(e.Err)
15 }
16 return GlobalError
17}
Operation
Operations define where the error occurred. When used effectively in combination with file lines, it can save hours of debugging time and searching through hundreds of files and lines.
The following format is preferred:
ServiceName.FunctionName
The service name is the name of the struct or type that’s implementing a function or package. And the function name is the name of the function that produced the error.
Below is an example of returning a formatted Error type in a database call for obtaining a singular user. If no rows are found, we return a NOTFOUND code. Likewise, if there was an error executing the SQL query, we return a NOTFOUND code with user-friendly messages.
Notice: We are defining op
at the top of the method, which when used consistently, can help you to easily find
where errors occurred extremely quickly.
1func (s *UserStore) Find(ctx context.Context, id int64) (core.User, error) {
2 const op = "UserStore.Find"
3
4 q := "SELECT from users WHERE ID = ? LIMIT 1"
5
6 var out core.User
7 err := s.DB().GetContext(ctx, &out, q.Build(), id)
8 if err == sql.ErrNoRows {
9 return core.User{}, errors.NewNotFound(err, fmt.Sprintf("Error obtaining User with the ID: %d", id), op)
10 } else if err != nil {
11 return core.User{}, errors.NewInternal(err, "Error executing SQL query", op)
12 }
13
14 return out, nil
15}
Stack traces
Stack traces report the active frames at a certain time during the execution of an application. They are very effective for debugging as developers can see the steps that led up to an error.
In the case of the custom Error
, we can implement a StackTrace()
method that returns a string slice of the stack by
traversing the frame lines.
1// StackTraceSlice returns a string slice of the errors
2// stacktrace.
3func (e *Error) StackTraceSlice() []string {
4 trace := make([]string, 0, 100)
5 rFrames := e.RuntimeFrames()
6 frame, ok := rFrames.Next()
7 line := strconv.Itoa(frame.Line)
8 trace = append(trace, frame.Function+"(): "+e.Message)
9
10 for ok {
11 trace = append(trace, frame.File+":"+line)
12 frame, ok = rFrames.Next()
13 }
14
15 return trace
16}
Wrapping up
By using a package similar to github.com/ainsleyclark/errors or by implementing your own errors package, you can be sure that hours of traversing directory trees are a thing of the past. Not only does proper and effective error handling save time for a developer, it makes things simpler and easier to understand for your end users. By using them consistently and correctly, you are able to speed up workflow and make things easier to reason about.