GoLangErrorsCoding

Effective error handling in GoLang; how to Error()

Ainsley Clark Photo

Ainsley Clark

Mar 14, 2022 - 7 min read.

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.

Useful links

About the author

Ainsley Clark Photo
Ainsley Clark

CEO & Director, Designer & Software Engineer

Ainsley Clark is a senior full-stack software engineer and web developer. As the company's founder, he is in charge of each key stage of our website and software builds and is our clients' reliable point of contact from start to finish of every project. During his extensive experience in the industry, Ainsley has helped numerous clients enhance their brand identity and increase their ROI with his custom designed websites, as well as constructed and deployed a vast array of highly scalable, tested and maintainable software applications, including SEO tools and APIs.

Follow me on: