As opposed to other languages like Python and JavaScript that use a try-catch approach to error handling, Go encourages us to return a separate value that will indicate that the code did not perform in an ideal manner - an error.
This way of handling errors ensures that dealing with errors are part of the normal execution flow of the code, instead of having a structure that can be completely left out, as is the case of a try-catch.
Convention
It is customary to return the error as the last value from a function, with the return type being error:
package main
import (
"errors"
"fmt"
)
func Div(a, b float64) (float64, error) {
if b == 0.0 {
return 0.0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
numbers := []int{3, 2, 0, -4, 5, -2}
for _, f := range numbers {
result, err := Div(4.0, float64(f))
if err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Printf("4/%d = %f\n", f, result)
}
}
}
The function errors.New() is used to define a new error with a desired error message. We can use the fmt.Errorf function to define errors as variables in our code:
package main
import (
"fmt"
)
var ErrNoZeroDivision = fmt.Errorf("cannot divide by zero")
func Div(a, b float64) (float64, error) {
if b == 0.0 {
return 0.0, ErrNoZeroDivision
}
return a / b, nil
}
func main() {
numbers := []int{3, 2, 0, -4, 5, -2}
for _, f := range numbers {
result, err := Div(4.0, float64(f))
if err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Printf("4/%d = %f\n", f, result)
}
}
}
Inline checking
We can check errors inline by separating the lines using a semi-colon:
package main
import (
"fmt"
)
var ErrNoZeroDivision = fmt.Errorf("cannot divide by zero")
func Div(a, b float64) (float64, error) {
if b == 0.0 {
return 0.0, ErrNoZeroDivision
}
return a / b, nil
}
func main() {
numbers := []int{3, 2, 0, -4, 5, -2}
for _, f := range numbers {
if result, err := Div(4.0, float64(f)); err != nil {
fmt.Printf("error: %v\n", err)
} else {
fmt.Printf("4/%d = %f\n", f, result)
}
}
}
Our code can look more compact using this technique.
Custom errors
Any custom type can be used as an error as long as it implements the Error() method.
package main
import (
"errors"
"fmt"
)
type fancyError struct {
arg int
message string
}
func (f *fancyError) Error() string {
return fmt.Sprintf("%d - %s", f.arg, f.message)
}
func f(arg int) (int, error) {
if arg == 42 {
return -1, &fancyError{arg, "can't work with it"}
}
return arg + 3, nil
}
func main() {
_, err := f(42)
var ae *fancyError
if errors.As(err, &ae) {
fmt.Println(ae.arg)
fmt.Println(ae.message)
} else {
fmt.Println("err doesn't match argError")
}
}
errors.Is checks to see if the error matches a certain value specifically. errors.As checks to see if the given error or any errors in the chain of errors matches a specific error type.
Wrapping and Unwrapping errors
Define a new error and wrap it with another error:
package main
import (
"errors"
"fmt"
)
var ErrAppRunningSlow = errors.New("your application is slowing down")
// function to wrap ErrAppRunningSlow
func checkVisitors() error {
return fmt.Errorf("too many visitors: %w", ErrAppRunningSlow)
}
// function to wrap the wrapped version of ErrAppRunningSlow
func checkVisitorsInGuyana() error {
err := checkVisitors()
return fmt.Errorf("Guyanese are visiting daily: %w", err)
}
func main() {
err := checkVisitorsInGuyana()
errInside := errors.Unwrap(err)
errInsideInside := errors.Unwrap(errInside)
someErr := errors.Unwrap(errInside)
otherErr := errors.Unwrap(errInside)
// print error wrapping the error wrapping ErrAppRunningSlow
fmt.Println(err)
// print error wrapping ErrAppRunningSlow
fmt.Println(errInside)
// print ErrAppRunningSlow (the innermost error)
fmt.Println(errInsideInside)
// print the innermost error
fmt.Println(someErr)
// print the innermost error
fmt.Println(otherErr)
}
Every call of the errors.Unwrap function, the inner error is returned. If the function is called with on the innermost error, it will keep on returning that error.