Error Handling in GoLang made easy

Harsh Nanchahal
6 min readJan 21, 2021

--

Introduction

From those coming from the world of other Object Oriented Languages like C# (Well Go is and is not your typical Object Oriented Language…another topic altogether), errors have been almost always synonymous to exceptions. In other languages, because you are uncertain if a function may throw an exception or not, you always end up wrapping your functions in try..catch. Instead of throwing exceptions, Go functions support multiple return values, and by convention, this ability is commonly used to return the function’s result along with an error variable.

func add(a,b int) (int, error){

If your function can fail for some reason, you should probably return the predeclared error type from it. The caller of your function knows they should not rely on the result before checking the error. If error is not nil, it is their responsibility to check for it and handle it (log, return, serve, invoke some retry/cleanup mechanism, etc.).

result,err := add(a,b)
if err != nil {
// handle error
}
// continue

This is one of the advantages of how errors work with Go. Handling of errors is part of the main code flow and not a separate section where you typically handle the errors in an exception block. So in some sense, it is hard to ignore error handling as you write code.

Under the hood

The error is a built-in type but in reality, it is an interface type made available globally and it implements Error method which returns a string error message.

type error interface {
Error() string
}

Because the zero value of an interface is nil, any type that implements the error interface is as an error.
Let’s create our first error by implementing error interface.

In the above example, we have created a struct type CustomError which implements Error method. This method returns a string. Hence, struct CustomError implements the error interface. So in nutshell, err is an error now. Println function is designed to call Error method automatically when the value is an error hence error message Error ! got printed.

Now you would ask, why to go through the pain of defining your custom struct to print such a simple error message. The above snippet is just to detail out what happens under the hood.
Go provides the built-in errors package which exports the New function. This function expects an error message and returns an error.

From the above example, we can see that the type of err is *errors.errorString which is a pointer to errors.errorString. When we see the value using %#v, myErr is a pointer to a struct which has s string field.

So theNew function return a pointer to the errorString struct invoked with the string passed to it. The errors package has errorString struct which implements the error interface.

type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}

The New function creates and returns the pointer to the errorString struct.

func New(text string) error {
return &errorString{text}
}

Different Error Handling techniques

Retry errors that can recover

There are errors that can resolve through retries — network glitches, IO operations, etc.

The following package can help resolve the pain with retries. https://godoc.org/github.com/cenkalti/backoff#example-Retry

ExponentialBackOff increases the retry time exponentially.

Adding context to an error

In the real world, your errors will be more than a simple string. They will need to have some context i.e where the error occurred.
Building on our earlier struct, let’s define an HttpError struct which has status and method fields. By implementing Error method, it implements the error interface. Here, Error method returns a detailed error message by using status and method values.

Let me walk through the above example one point at a time. First, we created the HttpError struct which has status and httpMethod fields. By implementing Error method, it implements the error interface. Here, Error method returns a detailed error message by using status and httpMethod values.

GetServerResponseis a mock function which is designed to send an HTTP request and return the response. But for our example, it returns empty response and an error. Here error is a pointer to the HttpError struct with 401 status and GET method fields.

Inside the main function, we call GetServerResponseis function which returns the response string (response) and an error (err). Since the dynamic value of the err has *HttpError type, henceerrval := err.(*HttpError) returns a pointer to the instance of the HttpError. Hence, errval contains all the contextual information.

Wrapping Errors

With the current package errors it is not possible to pass error information to the main function if there is chain of function calls, so the error context details can get lost.
That is where github.com/pkg/errors comes in. This library is compatible with errors but brings in some cool capabilities.

With github.com/pkg/errors you also some additional useful functions — errors.Unwrap and errors.Is

Logging Strategies

Golang’s default package log doesn’t provide the ability to log with the logging level. This is where Logrus can come handy.

Logrus also provide the capability to structure log output — this is a very handy capability as it provides developers the ability to add context to the error log message.

Stack Trace

Another key aspect of logging is the ability to get log stack trace. If you use github.com/pkg/errors, you could

logrus.Error("Error occurred", fmt.Sprintf("%+v", err))

to get the stack trace.

To panic or not?

In short, panic will cause the application to crash. When some unexpected issue happens, panic can be used. Mostly panic is being used to fail the application in case of any issue which interrupts the normal operation of the application. A perfect example we can think is a database transaction. Usually, the application would try to establish a connection with the database when initializing. But if the application fails to establish the connection with the database, the application can’t continue to function properly. So in this kind of scenarios, the application should panic.

Panic will result in a stack trace which will allow us to trace the error.

When not to panic

But consider an application which allows users to login. What if a user tries to login with an account which doesn’t exist in the database. In this kind of scenarios, we can’t panic. We have to handle the error gracefully. We can log the error with the login details that the user entered and return an error response to the user.

Summary

The Go community has been making impressive strides as of late with even more concise and easy ways to handle errors. Have you got any ideas on how to handle or work with errors that may appear in your Go program?

--

--