If you have read any Go code, you have probably seen something like this:
if err != nil {
return err
}
Some people are really bothered with this, but I think it’s one of the things that makes Go a very safe language. But in fact, there is so much more to error handling than just that, so I’ll describe some of the things I learned after all this time writing Go code.
- Do not panic!
Sometimes, an unexpected error may occur in our application and that will cause it to exit immediately. That is never a good thing, especially when running an API.
To solve this, Go provides us with function that catches panics and lets us handle them gracefully, like so:
func main() {
defer func() {
if r := recover(); r != nil {
log.Error("unexpected error", r)
}
}()
// run api
}
This way a goroutine will deal with the panic and prevent the API from shutting down, wich is great!
- Export errors to clients
When an error is returned from a function / method, we want to be able to know what kind of error happened to deal with it the proper way. The best way to do this is by exporting the errors for the client to handle, like so:
var (
ErrNotFound = errors.New("not found")
ErrInvalidParameter = errors.New("invalid parameter")
)
func Myfunc(parameters ...) error {
}
client code:
...
err := provider.Myfunc(parameters)
if err != nil {
if errors.Is(err, ErrNotFound) {
// deal with the not found error, returning 404 in http for example
}
if errors.Is(err, ErrInvalidParameter) {
// deal with the invalid parameter error, returning 403 in http for example
}
log.Error(err)
// default behavior for unexpected errors
}
this can also be done with a switch statement: client code:
...
err := provider.Myfunc(parameters)
switch {
case errors.Is(err, ErrNotFound):
// deal with the not found error, returning 404 in http for example
case errors.Is(err, ErrInvalidParameter):
// deal with the invalid parameter error, returning 403 in http for example
}
default:
log.Error(err)
// default behavior for unexpected errors
}
Another good way to do this is creating an internal error package and mapping all the behaviours, for example:
package ierrors
var (
ErrNotFound = errors.New("[404] not found")
ErrBadRequest = errors.New("[403] bad request")
ErrInternalServerError = error.New("[500] internal server error")
)
var errMap = map[error]error{
provider.ErrNotFound: ErrNotFound,
provider.ErrInvalidParameter: ErrBadRequest,
}
func HandleError(err error) {
if err == nil {
// do nothing
return
}
customError, ok := errMap[err]
if ok {
// deal with custom error, return to user, etc.
return
}
log.Error(err)
// deal with unexpected error
return
}
- Always wrap errors
Sometimes we need more information about some error to help us understand and debug our application better, but we also need to be able to check what the original error wars. To do so, we can use error wrapping and wrap information around the original error but maintaining the first value. Something like this:
var MyCustomError = errors.New("something bad")
func MyFunc() error {
...
return fmt.Errorf("my wrapped info message: %w", MyCustomError)
}
On the client side:
err := provider.Myfunc(parameters)
if err != nil {
if errors.Is(err, provider.MyCustomError) {
// even with the wrapped info, we can still check the original error
}
}
- Logging the error
The error logging should happen only on the upmost level that we have access to the error, that way we can ensure we don’t have duplicated log entries and we have access to all the wrapped information or all the details about that error behaviour.
Conclusion
Error handling in Go is a very powerful feature that, if done right, can help us debug and solve problems very efficiently, but it’s also very easy to make a mess and be overwhelmed by incoherent, inconsistent and unreliable errors. So make sure to always pay attention to to error handling from the beginning of your project to prevent unnecessary headaches.