One thing I love about Go is that it gives you so much with so little. For example, the errors package is one of the smallest packages in the standard library and still, it gets you covered like a Swiss knife.
In this article, I want to show you how easy is to handle errors in Go, and hopefully, by the end of it, you’ll feel as empowered as I am when working with them.
All the examples are in this GitHub repository if you want to test them locally, so now let’s dive in!
Simple text errors Link to heading
The easiest way to return an error from a function is to call errors.New.
Bellow, registerUser
returns a value of type error
by calling errors.New
:
package main
import (
"errors"
"log"
)
func main() {
if err := registerUser("Andrei", -1); err != nil {
log.Fatalf("error registering user: %s", err)
}
}
func registerUser(name string, age int) error {
if name == "" {
return errors.New("name is empty")
}
if age <= 0 {
return errors.New("age is <= 0")
}
return nil
}
error
in Go is an interface, which means that any type can implement it as long as it matches the interface
signature
which is just an Error method that returns a string:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
It’s worth noting that calling errors.New
with the same string results in two distinct errors. That’s because
the New
function returns an address, and no matter what string you put there, it will always return a different one. We can
check the standard library for the implementation details:
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
Error variables Link to heading
Error variables are still constructed with errors.New
, but because we store the return in separate variables it allows
the caller to check and handle individual errors:
package main
import (
"errors"
"fmt"
)
var (
ErrEmptyName = errors.New("name is empty")
ErrInvalidAge = errors.New("age is <= 0")
)
func main() {
err := registerUser("", 20)
// here we check for different errors
if errors.Is(err, ErrEmptyName) {
fmt.Println("Your name is empty")
}
if errors.Is(err, ErrInvalidAge) {
fmt.Println("Your age is invalid")
}
}
func registerUser(name string, age int) error {
if name == "" {
return ErrEmptyName
}
if age <= 0 {
return ErrInvalidAge
}
return nil
}
To check the value of err, we call the errors.Is
function which checks if the err is equal to its target.
Remember, errors in Go are values, and you can compare them the same way you would do with any value.
This approach is useful when you want to handle different errors in different ways, similar to how you would treat exceptions in other languages.
Error Types Link to heading
Sometimes error variables are not enough. Maybe you need a more complex type that can have several fields. That’s easily
done: just have a type that has the method Error() string
signature:
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
}
// ValidationError implements error interface due to this method
func (e ValidationError) Error() string {
return fmt.Sprintf("%s is invalid", e.Field)
}
func main() {
err := registerUser("", 20)
// checking that err is of type ValidationError
if errors.As(err, &ValidationError{}) {
fmt.Printf("Validation failed: %s\n", err)
}
}
func registerUser(name string, age int) error {
if name == "" {
return ValidationError{Field: "name"}
}
if age <= 0 {
return ValidationError{Field: "age"}
}
return nil
}
Above we defined a ValidationError
struct that implements the error interface, and we can check for the type using
errors.As function.
Remember that to check for a type T
, you need to use &T
in errors.As
for target.
errors.Is vs errors.As Link to heading
They sound similar, so they might confuse you at first, but to recap, remember that errors.Is
checks for equality and
errors.As
checks for type.
For example, I said in the beginning that errors.New
will return a different error even if we use the same string.
Let’s
test that inequality using errors.Is
:
package main
import (
"errors"
"fmt"
)
func main() {
errFirst := errors.New("hello")
errSecond := errors.New("hello")
if errors.Is(errFirst, errSecond) {
fmt.Println("they are equal")
} else {
fmt.Println("they are not equal")
}
}
As we can see they are not equal and errors.Is
is working as expected.
errors.As
will check for type and not only that: if the err
satisfies that type it sets the target to that error
value:
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s is invalid", e.Field)
}
func main() {
err := registerUser("", 20)
var errValidation ValidationError
if errors.As(err, &errValidation) { // will check for type and unpack err to errValidation
fmt.Println("err is of type ValidationError and has field:", errValidation.Field)
}
}
func registerUser(name string, age int) error {
if name == "" {
return ValidationError{Field: "name"}
}
if age <= 0 {
return ValidationError{Field: "age"}
}
return nil
}
When you have a custom error type with several fields, two variables of the same type can have different field values, so checking for equality no longer works. That’s why is useful to have a method to check for an error type.
Wrapped errors Link to heading
If you read the docs of errors.Is
and errors.As
you will see that it says something
like “Is reports whether any error
in err’s tree matches target." or “As finds the first error in err’s tree that matches target”.
Earlier, I said that Is
check for equality and As
checks for type. But that’s not the full picture.
Errors in Go can wrap(contain) other errors. So when you call Is
or As
for an err
against a target
, it will not
compare
only err
to target
, but also any wrapped error inside of err
against target
.
This lets you compose errors from other errors and you would still manage to handle them properly.
The simplest way to create an error that wraps another error is by calling fmt.ErrorF
and using the %w
operand for the
error you want to wrap:
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s is invalid", e.Field)
}
func main() {
err := registerUser("", 20)
//err is of type ValidationError because it's wrapping it
if errors.As(err, &ValidationError{}) {
fmt.Printf("Validation failed: %s\n", err)
}
}
func registerUser(name string, age int) error {
err := validate(name, age)
if err != nil {
// the error returned by fmt.Errorf will wrap err because we use %w
return fmt.Errorf("error on validation: %w", err)
}
return nil
}
func validate(name string, age int) error {
if name == "" {
return ValidationError{Field: "name"}
}
if age <= 0 {
return ValidationError{Field: "age"}
}
return nil
}
You can also use %w
multiple times thus allowing you to wrap multiple errors:
package main
import (
"errors"
"fmt"
)
func main() {
err := getTwoWrappedErrors()
if errors.As(err, &FirstError{}) {
fmt.Println("err is wrapping FirstError")
}
if errors.As(err, &SecondError{}) {
fmt.Println("err is wrapping SecondError")
}
}
func getTwoWrappedErrors() error {
var (
f FirstError
s SecondError
)
return fmt.Errorf("i am wrapping to errors: %w and %w", f, s)
}
type FirstError struct {
}
func (FirstError) Error() string {
return "i'm custom error"
}
type SecondError struct {
}
func (SecondError) Error() string {
return "i'm second error"
}
That’s all for now! I hope you can see how much flexibility we have with only 3 functions.
Further reading: