Error Handling in GoLang made easy
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.
GetServerResponse
is 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 GetServerResponse
is 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?