If not always annoying, errors are one heck of important things in any software development kit. Without them, you just cannot build a reliable software. That means handling them in your code becomes even more important.
Thankfully in Go, errors are first class citizens. While they are certainly a little verbose to deal with compared to other programming languages, they are simple enough.
Background of Go Errors
Go tour and this official blog post do a very good job of explaining what Go errors are and how they look like, so I wont' over them.
Now the blog post is quite old but just valid even today. How? Because Go has a silent and salient feature called Go's backward comptability. Meaning, your code written in an old version of Go can always be built by the latest verion of Go. Yes, if you come from NodeJS, you can feel the frustration of breaking changes even within the same major releases in Node, but not with Go.
Enough ... lets continue
In short, there is an inbuilt type called error and a package named errors which we can use to define new Error variables.
You can create an error using the errors package.
var err1 = errors.New("YOLO error")
Errors are also comparable.
import (
...
errors
)
var (
Err1 = errors.New("Error1")
)
func getErr1() error {
return Err1
}
func main() {
e1 := getErr1()
e2 := getErr1()
fmt.Println("e1 == e2", e1 == e2) // true
}
Error wraping or embedding
So if it ever is possible to grab an error from a function's return call, then it becomes easier to compare against existing errors and carry on with the business logic.
Simple enough, but in Go you can even embed an error inside another error.
var (
...
Err2 = fmt.Errorf("Err2 %w", Err1)
)
func getErr2() error {
return Err2
}
func main() {
...
err := getErr2()
fmt.Println("getErr2() == Err2", err == Err2)
}
Thanks to fmt.Errorf, it is possible to embed Err1 within Err2 using %w format. But how can I grab the underlying error from Err2?
Well, by Unwraping
func main() {
...
fmt.Println("errors.Unwrap(getErr2()) == Err1", errors.Unwrap(err) == Err1) // true
}
Unwrap does exactly what it says on the tin, it unwraps the current error. But it only unwraps one error at a time.
var (
...
Err3 = fmt.Errorf("Err3 %w", Err2)
)
func getErr3() {
return Err3
}
func main() {
...
err = getErr3()
// What if I want to know if Err3 contains Err1?
// I can easily get Err2. But what about Err1?
fmt.Println("errors.Unwrap(getErr3()) == Err2 ", errors.Unwrap(getErr3()) == Err2)
// I Unwrap the unwrapped
fmt.Println("errors.Unwrap(errors.Unwrap(getErr3())) == Err1", errors.Unwrap(errors.Unwrap(err)) == Err1)
}
As you noticed, if you need to dig in on the error from a dense stack of errors' chain, then using only Unwrap will be quite bothersome. But no worries, Go has a solution for this as well.
Use errors.Is(...)
...
func main() {
...
...
fmt.Println("errors.Is(gettErr3(), Err1)", errors.Is(getErr3(), Err1)) // true
}
Hence, when you have unknown levels of embedded error stack, then the above method becomes very handy to pull out the exact error we need. However, if the error stack too dense, there may be performance implications if used too often.
Custom error types
In Go because error is an interface type. Any type that implements Error method becomes error type as well.
type myError struct {
extra string
}
func (me myError) Error() string {
return fmt.Sprintf("myError with some extra data: [%s]", me.extra)
}
We can now do some intersting stuff, like embedding additional error in our custom type.
...
type myError struct {
extra string
err error
}
func (me myError) Error() string {
return fmt.Sprintf("myError has extra data: [%s] with the underlying err: [%s]", me.extra, me.err)
}
func getMyError(info string, err error) myError {
return myError{info, err}
}
func main() {
...
myErr := getMyError("Some extra data", Err1)
}
But how can I know if myErr is my custom myError type?
Using errors.As(...)
...
func main() {
...
myCustomErr := &myError{}
ok := errors.As(myErr, myCustomErr)
if ok {
fmt.Println("myCustomErr ", myCustomErr)
} else {
...
}
}
Very straight forward indeed. Just create a new variable and unload the error in here and check the boolean to see if that actually worked.
You can now pull the underlying error and run through the Unwrap on that error
...
func main() {
...
errors.Unwrap(myCustomErr.err)
}
Conclusion
As you can see, error handling in Go is pretty nice. With such a simple design you can write complex logic by propagating errors in your logic flow.