Handling Errors in Go
Go’s error handling is just different. If you are coming from different language background like PHP, I’m quite sure that just like me, you had to change your entire approach on error handling and begin what feels almost unnatural.
Values, not exceptions
Go handles errors by returning an error value, instead of the usual exceptions we’re used to. A much different approach from the try… catch blocks, it handles errors without any noticeable change in the flow of your program.
The code above shows a basic error handling operation. In this sense, the ‘err’ variable has a simple value that may be anything. This is similar to a PHP code that returns an array which may have an error key:
Returning an error from your function
Just like you can add an errors key in the PHP code above, Go functions can return tuples whose values can be manipulated. Here’s an example of using Go functions to return tuples, one of which is an error:
The Roulette function returns both a string and an error, but selects the outcome based on a random number generator. In cases where the caller fails, the function returns an empty string (zero type of string) and a new error using the Go’s errors package. If the caller wins, the function returns a success message and a nil error value (zero value of error interface type).
The Go error interface
An interface simply defines a set of methods that must be implemented to satisfy that given type. It borrows from generic interfaces in other language, with some major differences.
The Go error interface has a single method named Error that simply returns an error message as a string. With this interface, you can simply define custom errors:
Informative Error Handling
Sometimes we may need more than what conventional error handling has to offer. For example, the custom error created above can no longer be compared to an original error type that may give us more information about the errors.
A very good package to take care of this problem is the pkg/errors package [https://pkg.go.dev/github.com/pkg/errors]. This package allows you wrap errors around custom messages and returns the underlying cause of the error. This way, your custom errors can exist, alongside the original error provided by the package.
In the code above, we created a custom error with errors.Wrap(). This new error behaves like an actual error, but can still access the underlying error, which in this case is an *os.PathError type. Here’s the full code with the underlying error access:
In this case, we were able to use type switching and the errors.Cause() function to extract and compare the underlying error of our custom error.
How to Properly Log errors
Errors should be well detailed so that anyone examining the error logs knows exactly what happened, where it happened, when it happened and why it happened.
The 4 Ws gives a better understanding of the problem and helps the developer know exactly what to do. Here’s a simple implementation of this idea:
In this code, we were able to use the pkg/errors package to wrap the file open error. Then we reported the error message satisfying the four Ws mentioned above:
- When: The log package appends the time the log was made, to the second.
- What: Stated exactly what happened: “ProcessFile(name) failed”
- Why it happened: “no such file or directory”
- Where it happened: The pkg/errors package adds the callstack to the error, this helps pinpoints the specific files and lines the errors happened.
(Please note that adding callstack to errors is expensive and primarily useful in debugging. Also, io.Copy returns an error value which was discarded for simplicity)
I hope this gives a much clearer understanding of errors and helps you handle errors better in your future projects.
All examples are hosted on this repository: https://github.com/joshuaetim/errorhandler