Package errors implements functions to manipulate errors.
The New function creates errors whose only content is a text message.
An error e wraps another error if e’s type has one of the methods
Unwrap() error Unwrap() []error
If e.Unwrap() returns a non-nil error w or a slice containing w,
then we say that e wraps w. A nil error returned from e.Unwrap()
indicates that e does not wrap any error. It is invalid for an
Unwrap method to return an []error containing a nil error value.
An easy way to create wrapped errors is to call fmt.Errorf and apply
the %w verb to the error argument:
wrapsErr := fmt.Errorf("... %w ...", ..., err, ...)
Successive unwrapping of an error creates a tree. The Is and As
functions inspect an error’s tree by examining first the error
itself followed by the tree of each of its children in turn
(pre-order, depth-first traversal).
Is examines the tree of its first argument looking for an error that
matches the second. It reports whether it finds a match. It should be
used in preference to simple equality checks:
if errors.Is(err, fs.ErrExist)
is preferable to
if err == fs.ErrExist
because the former will succeed if err wraps fs.ErrExist.
As examines the tree of its first argument looking for an error that can be
assigned to its second argument, which must be a pointer. If it succeeds, it
performs the assignment and returns true. Otherwise, it returns false. The form
var perr *fs.PathError if errors.As(err, &perr) { fmt.Println(perr.Path) }
is preferable to
if perr, ok := err.(*fs.PathError); ok { fmt.Println(perr.Path) }
because the former will succeed if err wraps an *fs.PathError.
package main import ( "fmt" "time" ) // MyError is an error implementation that includes a time and message. type MyError struct { When time.Time What string } func (e MyError) Error() string { return fmt.Sprintf("%v: %v", e.When, e.What) } func oops() error { return MyError{ time.Date(1989, 3, 15, 22, 30, 0, 0, time.UTC), "the file system has gone away", } } func main() { if err := oops(); err != nil { fmt.Println(err) } }
Output: 1989-03-15 22:30:00 +0000 UTC: the file system has gone away
- func As(err error, target any) bool
- func Is(err, target error) bool
- func Join(errs …error) error
- func New(text string) error
- func Unwrap(err error) error
- Package
- As
- Is
- Join
- New
- New (Errorf)
- Unwrap
This section is empty.
This section is empty.
As finds the first error in err’s tree that matches target, and if one is found, sets
target to that error value and returns true. Otherwise, it returns false.
The tree consists of err itself, followed by the errors obtained by repeatedly
calling Unwrap. When err wraps multiple errors, As examines err followed by a
depth-first traversal of its children.
An error matches target if the error’s concrete value is assignable to the value
pointed to by target, or if the error has a method As(interface{}) bool such that
As(target) returns true. In the latter case, the As method is responsible for
setting target.
An error type might provide an As method so it can be treated as if it were a
different error type.
As panics if target is not a non-nil pointer to either a type that implements
error, or to any interface type.
package main import ( "errors" "fmt" "io/fs" "os" ) func main() { if _, err := os.Open("non-existing"); err != nil { var pathError *fs.PathError if errors.As(err, &pathError) { fmt.Println("Failed at path:", pathError.Path) } else { fmt.Println(err) } } }
Output: Failed at path: non-existing
Is reports whether any error in err’s tree matches target.
The tree consists of err itself, followed by the errors obtained by repeatedly
calling Unwrap. When err wraps multiple errors, Is examines err followed by a
depth-first traversal of its children.
An error is considered to match a target if it is equal to that target or if
it implements a method Is(error) bool such that Is(target) returns true.
An error type might provide an Is method so it can be treated as equivalent
to an existing error. For example, if MyError defines
func (m MyError) Is(target error) bool { return target == fs.ErrExist }
then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for
an example in the standard library. An Is method should only shallowly
compare err and the target and not call Unwrap on either.
package main import ( "errors" "fmt" "io/fs" "os" ) func main() { if _, err := os.Open("non-existing"); err != nil { if errors.Is(err, fs.ErrNotExist) { fmt.Println("file does not exist") } else { fmt.Println(err) } } }
Output: file does not exist
Join returns an error that wraps the given errors.
Any nil error values are discarded.
Join returns nil if errs contains no non-nil values.
The error formats as the concatenation of the strings obtained
by calling the Error method of each element of errs, with a newline
between each string.
package main import ( "errors" "fmt" ) func main() { err1 := errors.New("err1") err2 := errors.New("err2") err := errors.Join(err1, err2) fmt.Println(err) if errors.Is(err, err1) { fmt.Println("err is err1") } if errors.Is(err, err2) { fmt.Println("err is err2") } }
Output: err1 err2 err is err1 err is err2
New returns an error that formats as the given text.
Each call to New returns a distinct error value even if the text is identical.
package main import ( "errors" "fmt" ) func main() { err := errors.New("emit macho dwarf: elf header corrupted") if err != nil { fmt.Print(err) } }
Output: emit macho dwarf: elf header corrupted
The fmt package’s Errorf function lets us use the package’s formatting
features to create descriptive error messages.
package main import ( "fmt" ) func main() { const name, id = "bimmler", 17 err := fmt.Errorf("user %q (id %d) not found", name, id) if err != nil { fmt.Print(err) } }
Output: user "bimmler" (id 17) not found
Unwrap returns the result of calling the Unwrap method on err, if err’s
type contains an Unwrap method returning error.
Otherwise, Unwrap returns nil.
Unwrap returns nil if the Unwrap method returns []error.
package main import ( "errors" "fmt" ) func main() { err1 := errors.New("error1") err2 := fmt.Errorf("error2: [%w]", err1) fmt.Println(err2) fmt.Println(errors.Unwrap(err2)) // Output // error2: [error1] // error1 }
Output:
This section is empty.
Welcome to tutorial no. 31 in our Golang tutorial series.
In the last tutorial we learnt about error representation in Go and how to handle errors from the standard library. We also learnt how to extract more information from the errors.
This tutorial deals with how to create our own custom errors which we can use in our functions and packages. We will also use the same techniques employed by the standard library to provide more details about our custom errors.
Creating custom errors using the New function
The simplest way to create a custom error is to use the New function of the errors package.
Before we use the New function to create a custom error, let’s understand how it is implemented. The implementation of the New function in the errors package is provided below.
package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
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
}
The implementation is pretty simple. errorString
is a struct type with a single string field s
. The Error() string
method of the error
interface is implemented using a errorString
pointer receiver in line no. 14.
The New
function in line no. 5 takes a string
parameter, creates a value of type errorString
using that parameter and returns the address of it. Thus a new error is created and returned.
Now that we know how the New
function works, lets use it in a program of our own to create a custom error.
We will create a simple program which calculates the area of a circle and will return an error if the radius is negative.
package main
import (
"errors"
"fmt"
"math"
)
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, errors.New("Area calculation failed, radius is less than zero")
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Area of circle %0.2f", area)
}
Run in playground
In the program above, we check whether the radius is less than zero in line no. 10. If so we return zero for the area along with the corresponding error message. If the radius is greater than 0, then the area is calculated and nil
is returned as the error in line no. 13.
In the main function, we check whether the error is not nil
in line no. 19. If it’s not nil
, we print the error and return, else the area of the circle is printed.
In this program the radius is less than zero and hence it will print,
Area calculation failed, radius is less than zero
Adding more information to the error using Errorf
The above program works well but wouldn’t it be nice if we print the actual radius which caused the error. This is where the Errorf function of the fmt package comes in handy. This function formats the error according to a format specifier and returns a string as value that satisfies the error
interface.
Let’s use the Errorf
function and make the program better.
package main
import (
"fmt"
"math"
)
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius)
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Area of circle %0.2f", area)
}
Run in playground
In the program above, the Errorf
is used in line no. 10 to print the actual radius which caused the error. Running this program will output,
Area calculation failed, radius -20.00 is less than zero
Providing more information about the error using struct type and fields
It is also possible to use struct types which implement the error interface as errors. This gives us more flexibility with error handling. In our previous example, if we want to access the radius which caused the error, the only way now is to parse the error description Area calculation failed, radius -20.00 is less than zero
. This is not a proper way to do this since if the description changes, our code will break.
We will use the strategy followed by the standard library explained in the previous tutorial under the section «Converting the error to the underlying type and retrieving more information from the struct fields» and use struct fields to provide access to the radius which caused the error. We will create a struct type that implements the error interface and use its fields to provide more information about the error.
The first step would be create a struct type to represent the error. The naming convention for error types is that the name should end with the text Error
. So let’s name our struct type as areaError
type areaError struct {
err string
radius float64
}
The above struct type has a field radius
which stores the value of the radius responsible for the error and err
field stores the actual error message.
The next step is to implement the error interface.
func (e *areaError) Error() string {
return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}
In the above snippet, we implement the Error() string
method of the error interface using a pointer receiver *areaError
. This method prints the radius and the error description.
Let’s complete the program by writing the main
function and circleArea
function.
package main
import (
"errors"
"fmt"
"math"
)
type areaError struct {
err string
radius float64
}
func (e *areaError) Error() string {
return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, &areaError{
err: "radius is negative",
radius: radius,
}
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
var areaError *areaError
if errors.As(err, &areaError) {
fmt.Printf("Area calculation failed, radius %0.2f is less than zero", areaError.radius)
return
}
fmt.Println(err)
return
}
fmt.Printf("Area of rectangle %0.2f", area)
}
Run in playground
In the program above, circleArea
in line no. 18 is used to calculate the area of the circle. This function first checks if the radius is less than zero, if so it creates a value of type areaError
using the radius responsible for the error and the corresponding error message and then returns the address of it in line no. 20 along with 0
as area. Thus we have provided more information about the error, in this case the radius which caused the error using the fields of a custom error struct.
If the radius is not negative, this function calculates and returns the area along with a nil
error in line no. 25.
In line no. 30 of the main function, we are trying to find the area of a circle with radius -20. Since the radius is less than zero, an error will be returned.
We check whether the error is not nil
in line no. 31 and in line no. 33 line we try to convert it to type *areaError
. If the error is of type *areaError
, we get the radius which caused the error in line no. 34 using areaError.radius
, print a custom error message and return from the program.
If the error is not of type *areaError
, we simply print the error in line no. 37 and return. If there is no error, the area will be printed in line no.40.
The program will print,
Area calculation failed, radius -20.00 is less than zero
Now lets use the second strategy described in the previous tutorial and use methods on custom error types to provide more information about the error.
Providing more information about the error using methods on struct types
In this section we will write a program which calculates the area of a rectangle. This program will print an error if either the length or width is less than zero.
The first step would be create a struct to represent the error.
type areaError struct {
err string //error description
length float64 //length which caused the error
width float64 //width which caused the error
}
The above error struct type contains an error description field along with the length and width which caused the error.
Now that we have the error type, lets implement the error interface and add a couple of methods on the error type to provide more information about the error.
func (e *areaError) Error() string {
return e.err
}
func (e *areaError) lengthNegative() bool {
return e.length < 0
}
func (e *areaError) widthNegative() bool {
return e.width < 0
}
In the above snippet, we return the description of the error from the Error() string
method. The lengthNegative() bool
method returns true when the length is less than zero and widthNegative() bool
method returns true when the width is less than zero. These two methods provide more information about the error, in this case they say whether the area calculation failed because of the length being negative or width being negative. Thus we have used methods on struct error types to provide more information about the error.
The next step is to write the area calculation function.
func rectArea(length, width float64) (float64, error) {
err := ""
if length < 0 {
err += "length is less than zero"
}
if width < 0 {
if err == "" {
err = "width is less than zero"
} else {
err += ", width is less than zero"
}
}
if err != "" {
return 0, &areaError{
err: err,
length: length,
width: width,
}
}
return length * width, nil
}
The rectArea
function above checks if either the length or width is less than zero, if so it returns an error of type *areaError
, else it returns the area of the rectangle with nil
as error.
Let’s finish this program by creating the main function.
func main() {
length, width := -5.0, -9.0
area, err := rectArea(length, width)
if err != nil {
var areaError *areaError
if errors.As(err, &areaError) {
if areaError.lengthNegative() {
fmt.Printf("error: length %0.2f is less than zeron", areaError.length)
}
if areaError.widthNegative() {
fmt.Printf("error: width %0.2f is less than zeron", areaError.width)
}
return
}
fmt.Println(err)
return
}
fmt.Println("area of rect", area)
}
In the main function, we check whether the error is not nil
in line no. 4. If it is not nil, we try to convert it to type *areaError
. Then using the lengthNegative()
and widthNegative()
methods, we check whether the error is because of the fact that the length is negative or width is negative. We print the corresponding error message and return from the program. Thus we have used the methods on the error struct type to provide more information about the error.
If there is no error, the area of the rectangle will be printed.
Here is the full program for your reference.
package main
import (
"errors"
"fmt"
)
type areaError struct {
err string //error description
length float64 //length which caused the error
width float64 //width which caused the error
}
func (e *areaError) Error() string {
return e.err
}
func (e *areaError) lengthNegative() bool {
return e.length < 0
}
func (e *areaError) widthNegative() bool {
return e.width < 0
}
func rectArea(length, width float64) (float64, error) {
err := ""
if length < 0 {
err += "length is less than zero"
}
if width < 0 {
if err == "" {
err = "width is less than zero"
} else {
err += ", width is less than zero"
}
}
if err != "" {
return 0, &areaError{
err: err,
length: length,
width: width,
}
}
return length * width, nil
}
func main() {
length, width := -5.0, -9.0
area, err := rectArea(length, width)
if err != nil {
var areaError *areaError
if errors.As(err, &areaError) {
if areaError.lengthNegative() {
fmt.Printf("error: length %0.2f is less than zeron", areaError.length)
}
if areaError.widthNegative() {
fmt.Printf("error: width %0.2f is less than zeron", areaError.width)
}
return
}
fmt.Println(err)
return
}
fmt.Println("area of rect", area)
}
Run in playground
This program will print the output,
error: length -5.00 is less than zero
error: width -9.00 is less than zero
We have seen examples for two of the three ways described in the error handling tutorial to provide more information about the errors.
The third way using direct comparison is pretty straightforward. I would leave it as an exercise for you to figure out how to use this strategy to provide more information about our custom errors.
This brings us to an end of this tutorial.
Here is a quick recap of what we learnt in this tutorial,
- Creating custom errors using the New function
- Adding more information to the error using Errorf
- Providing more information about the error using struct type and fields
- Providing more information about the error using methods on struct types
Have a good day.
Next tutorial — Panic and Recover
In this article, we’ll take a look at how to handle errors using build-in Golang functionality, how you can extract information from the errors you are receiving and the best practices to do so.
Error handling in Golang is unconventional when compared to other mainstream languages like Javascript, Java and Python. This can make it very difficult for new programmers to grasp Golangs approach of tackling error handling.
In this article, we’ll take a look at how to handle errors using build-in Golang functionality, how you can extract information from the errors you are receiving and the best practices to do so. A basic understanding of Golang is therefore required to follow this article. If you are unsure about any concepts, you can look them up here.
Errors in Golang
Errors indicate an unwanted condition occurring in your application. Let’s say you want to create a temporary directory where you can store some files for your application, but the directory’s creation fails. This is an unwanted condition and is therefore represented using an error.
package main
import (
"fmt"
"ioutil"
)
func main() {
dir, err := ioutil.TempDir("", "temp")
if err != nil {
return fmt.Errorf("failed to create temp dir: %v", err)
}
}
Golang represents errors using the built-in error type, which we will look at closer in the next section. The error is often returned as a second argument of the function, as shown in the example above. Here the TempDir function returns the name of the directory as well as an error variable.
Creating custom errors
As already mentioned errors are represented using the built-in error interface type, which has the following definition:
type error interface {
Error() string
}
The interface contains a single method Error() that returns an error message as a string. Every type that implements the error interface can be used as an error. When printing the error using methods like fmt.Println the Error() method is automatically called by Golang.
There are multiple ways of creating custom error messages in Golang, each with its own advantages and disadvantages.
String-based errors
String-based errors can be created using two out-of-the-box options in Golang and are used for simple errors that just need to return an error message.
err := errors.New("math: divided by zero")
The errors.New() method can be used to create new errors and takes the error message as its only parameter.
err2 := fmt.Errorf("math: %g cannot be divided by zero", x)
fmt.Errorf on the other hand also provides the ability to add formatting to your error message. Above you can see that a parameter can be passed which will be included in the error message.
Custom error with data
You can create your own error type by implementing the Error() function defined in the error interface on your struct. Here is an example:
type PathError struct {
Path string
}
func (e *PathError) Error() string {
return fmt.Sprintf("error in path: %v", e.Path)
}
The PathError implements the Error() function and therefore satisfies the error interface. The implementation of the Error() function now returns a string with the path of the PathError struct. You can now use PathError whenever you want to throw an error.
Here is an elementary example:
package main
import(
"fmt"
)
type PathError struct {
Path string
}
func (e *PathError) Error() string {
return fmt.Sprintf("error in path: %v", e.Path)
}
func throwError() error {
return &PathError{Path: "/test"}
}
func main() {
err := throwError()
if err != nil {
fmt.Println(err)
}
}
You can also check if the error has a specific type using either an if or switch statement:
if err != nil {
switch e := err.(type) {
case *PathError :
// Do something with the path
default:
log.Println(e)
}
}
This will allow you to extract more information from your errors because you can then call all functions that are implemented on the specific error type. For example, if the PathError had a second method called GetInfo you could call it like this.
e.GetInfo()
Error handling in functions
Now that you know how to create your own custom errors and extract as much information as possible from errors let’s take a look at how you can handle errors in functions.
Most of the time errors are not directly handled in functions but are returned as a return value instead. Here we can take advantage of the fact that Golang supports multiple return values for a function. Thus you can return your error alongside the normal result — errors are always returned as the last argument — of the function as follows:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0.0, errors.New("cannot divide through zero")
}
return a/b, nil
}
The function call will then look similar to this:
func main() {
num, err := divide(100, 0)
if err != nil {
fmt.Printf("error: %s", err.Error())
} else {
fmt.Println("Number: ", num)
}
}
If the returned error is not nil it usually means that there is a problem and you need to handle the error appropriately. This can mean that you use some kind of log message to warn the user, retry the function until it works or close the application entirely depending on the situation. The only drawback is that Golang does not enforce handling the retuned errors, which means that you could just ignore handling errors completely.
Take the following code for example:
package main
import (
"errors"
"fmt"
)
func main() {
num2, _ := divide(100, 0)
fmt.Println("Number: ", num2)
}
The so-called blank identifier is used as an anonymous placeholder and therefore provides a way to ignore values in an assignment and avoid compiler errors in the process. But remember that using the blank identifier instead of probably handling errors is dangerous and should not be done if it can be avoided.
Defer, panic and recover
Go does not have exceptions like many other programming languages, including Java and Javascript but has a comparable mechanism know as ,,Defer, panic and recover». Still the use-cases of panic and recover are very different from exceptions in other programming languages as they should only be used in unexpected and unrecoverable situations.
Defer
A defer statement is a mechanism used to defer a function by putting it into an executed stack once the function that contains the defer statement has finished, either normally by executing a return statement or abnormally panicking. Deferred functions will then be executed in reverse order in which they were deferred.
Take the following function for example:
func processHTML(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
resp.Body.Close()
return fmt.Errorf("%s has content type %s which does not match text/html", url, ct)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
// ... Process HTML ...
return nil
}
Here you can notice the duplicated resp.Body.Close call, which ensures that the response is properly closed. Once functions grow more complex and have more errors that need to be handled such duplications get more and more problematic to maintain.
Since deferred calls get called once the function has ended, no matter if it succeeded or not it can be used to simplify such calls.
func processHTMLDefer(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
return fmt.Errorf("%s has content type %s which does not match text/html", url, ct)
}
doc, err := html.Parse(resp.Body)
// ... Process HTML ...
return nil
}
All deferred functions are executed in reverse order in which they were deferred when the function finishes.
package main
import (
"fmt"
)
func main() {
first()
}
func first() {
defer fmt.Println("first")
second()
}
func second() {
defer fmt.Println("second")
third()
}
func third() {
defer fmt.Println("third")
}
Here is the result of running the above program:
third
second
first
Panic
A panic statement signals Golang that your code cannot solve the current problem and it therefore stops the normal execution flow of your code. Once a panic is called, all deferred functions are executed and the program crashes with a log message that includes the panic values (usually an error message) and a stack trace.
As an example Golang will panic when a number is divided by zero.
package main
import "fmt"
func main() {
divide(5)
}
func divide(x int) {
fmt.Printf("divide(%d) n", x+0/x)
divide(x-1)
}
Once the divide function is called using zero, the program will panic, resulting in the following output.
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.divide(0x0)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:16 +0xe6
main.divide(0x1)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x2)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x3)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x4)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x5)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.main()
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:11 +0x31
exit status 2
You can also use the built-in panic function to panic in your own programms. A panic should mostly only be used when something happens that the program didn’t expect and cannot handle.
func getArguments() {
if len(os.Args) == 1 {
panic("Not enough arguments!")
}
}
As already mentioned, deferred functions will be executed before terminating the application, as shown in the following example.
package main
import (
"fmt"
)
func main() {
accessSlice([]int{1,2,5,6,7,8}, 0)
}
func accessSlice(slice []int, index int) {
fmt.Printf("item %d, value %d n", index, slice[index])
defer fmt.Printf("defer %d n", index)
accessSlice(slice, index+1)
}
Here is the output of the programm:
item 0, value 1
item 1, value 2
item 2, value 5
item 3, value 6
item 4, value 7
item 5, value 8
defer 5
defer 4
defer 3
defer 2
defer 1
defer 0
panic: runtime error: index out of range [6] with length 6
goroutine 1 [running]:
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x6)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:29 +0x250
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x5)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x4)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x3)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x2)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x1)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x0)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.main()
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:9 +0x99
exit status 2
Recover
In some rare cases panics should not terminate the application but be recovered instead. For example, a socket server that encounters an unexpected problem could report the error to the clients and then close all connections rather than leaving the clients wondering what just happened.
Panics can therefore be recovered by calling the built-in recover function within a deferred function in the function that is panicking. Recover will then end the current state of panic and return the panic error value.
package main
import "fmt"
func main(){
accessSlice([]int{1,2,5,6,7,8}, 0)
}
func accessSlice(slice []int, index int) {
defer func() {
if p := recover(); p != nil {
fmt.Printf("internal error: %v", p)
}
}()
fmt.Printf("item %d, value %d n", index, slice[index])
defer fmt.Printf("defer %d n", index)
accessSlice(slice, index+1)
}
As you can see after adding a recover function to the function we coded above the program doesn’t exit anymore when the index is out of bounds by recovers instead.
Output:
item 0, value 1
item 1, value 2
item 2, value 5
item 3, value 6
item 4, value 7
item 5, value 8
internal error: runtime error: index out of range [6] with length 6defer 5
defer 4
defer 3
defer 2
defer 1
defer 0
Recovering from panics can be useful in some cases, but as a general rule you should try to avoid recovering from panics.
Error wrapping
Golang also allows errors to wrap other errors which provides the functionality to provide additional context to your error messages. This is often used to provide specific information like where the error originated in your program.
You can create wrapped errors by using the %w flag with the fmt.Errorf function as shown in the following example.
package main
import (
"errors"
"fmt"
"os"
)
func main() {
err := openFile("non-existing")
if err != nil {
fmt.Printf("error running program: %s n", err.Error())
}
}
func openFile(filename string) error {
if _, err := os.Open(filename); err != nil {
return fmt.Errorf("error opening %s: %w", filename, err)
}
return nil
}
The output of the application would now look like the following:
error running program: error opening non-existing: open non-existing: no such file or directory
As you can see the application prints both the new error created using fmt.Errorf as well as the old error message that was passed to the %w flag. Golang also provides the functionality to get the old error message back by unwrapping the error using errors.Unwrap.
package main
import (
"errors"
"fmt"
"os"
)
func main() {
err := openFile("non-existing")
if err != nil {
fmt.Printf("error running program: %s n", err.Error())
// Unwrap error
unwrappedErr := errors.Unwrap(err)
fmt.Printf("unwrapped error: %v n", unwrappedErr)
}
}
func openFile(filename string) error {
if _, err := os.Open(filename); err != nil {
return fmt.Errorf("error opening %s: %w", filename, err)
}
return nil
}
As you can see the output now also displays the original error.
error running program: error opening non-existing: open non-existing: no such file or directory
unwrapped error: open non-existing: no such file or directory
Errors can be wrapped and unwrapped multiple times, but in most cases wrapping them more than a few times does not make sense.
Casting Errors
Sometimes you will need a way to cast between different error types to for example, access unique information that only that type has. The errors.As function provides an easy and safe way to do so by looking for the first error in the error chain that fits the requirements of the error type. If no match is found the function returns false.
Let’s look at the official errors.As docs example to better understand what is happening.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main(){
// Casting error
if _, err := os.Open("non-existing"); err != nil {
var pathError *os.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
}
Here we try to cast our generic error type to os.PathError so we can access the Path variable that that specific error contains.
Another useful functionality is checking if an error has a specific type. Golang provides the errors.Is function to do exactly that. Here you provide your error as well as the particular error type you want to check. If the error matches the specific type the function will return true, if not it will return false.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main(){
// Check if error is a specific type
if _, err := os.Open("non-existing"); err != nil {
if errors.Is(err, fs.ErrNotExist) {
fmt.Println("file does not exist")
} else {
fmt.Println(err)
}
}
}
After checking, you can adapt your error message accordingly.
Sources
- Golang Blog — Working with Errors in Go 1.13
- The Go Programming language book
- Golang Blog — Defer, Panic, and Recover
- LogRocket — Error handling in Golang
- GolangByExample — Wrapping and Un-wrapping of error in Go
- Golang Documentation — Package errors
Conclusion
You made it all the way until the end! I hope this article helped you understand the basics of Go error handling and why it is an essential topic in application/software development.
If you have found this helpful, please consider recommending and sharing it with other fellow developers and subscribing to my newsletter. If you have any questions or feedback, let me know using my contact form or contact me on Twitter.
import "errors"
- Overview
- Index
- Examples
Overview ▸
Overview ▾
Package errors implements functions to manipulate errors.
The New function creates errors whose only content is a text message.
An error e wraps another error if e’s type has one of the methods
Unwrap() error Unwrap() []error
If e.Unwrap() returns a non-nil error w or a slice containing w,
then we say that e wraps w. A nil error returned from e.Unwrap()
indicates that e does not wrap any error. It is invalid for an
Unwrap method to return an []error containing a nil error value.
An easy way to create wrapped errors is to call fmt.Errorf and apply
the %w verb to the error argument:
wrapsErr := fmt.Errorf("... %w ...", ..., err, ...)
Successive unwrapping of an error creates a tree. The Is and As
functions inspect an error’s tree by examining first the error
itself followed by the tree of each of its children in turn
(pre-order, depth-first traversal).
Is examines the tree of its first argument looking for an error that
matches the second. It reports whether it finds a match. It should be
used in preference to simple equality checks:
if errors.Is(err, fs.ErrExist)
is preferable to
if err == fs.ErrExist
because the former will succeed if err wraps fs.ErrExist.
As examines the tree of its first argument looking for an error that can be
assigned to its second argument, which must be a pointer. If it succeeds, it
performs the assignment and returns true. Otherwise, it returns false. The form
var perr *fs.PathError if errors.As(err, &perr) { fmt.Println(perr.Path) }
is preferable to
if perr, ok := err.(*fs.PathError); ok { fmt.Println(perr.Path) }
because the former will succeed if err wraps an *fs.PathError.
▸ Example
▾ Example
1989-03-15 22:30:00 +0000 UTC: the file system has gone away
Index ▸
func As
¶
1.13
func As(err error, target any) bool
As finds the first error in err’s tree that matches target, and if one is found, sets
target to that error value and returns true. Otherwise, it returns false.
The tree consists of err itself, followed by the errors obtained by repeatedly
calling Unwrap. When err wraps multiple errors, As examines err followed by a
depth-first traversal of its children.
An error matches target if the error’s concrete value is assignable to the value
pointed to by target, or if the error has a method As(interface{}) bool such that
As(target) returns true. In the latter case, the As method is responsible for
setting target.
An error type might provide an As method so it can be treated as if it were a
different error type.
As panics if target is not a non-nil pointer to either a type that implements
error, or to any interface type.
▸ Example
▾ Example
Failed at path: non-existing
func Is
¶
1.13
func Is(err, target error) bool
Is reports whether any error in err’s tree matches target.
The tree consists of err itself, followed by the errors obtained by repeatedly
calling Unwrap. When err wraps multiple errors, Is examines err followed by a
depth-first traversal of its children.
An error is considered to match a target if it is equal to that target or if
it implements a method Is(error) bool such that Is(target) returns true.
An error type might provide an Is method so it can be treated as equivalent
to an existing error. For example, if MyError defines
func (m MyError) Is(target error) bool { return target == fs.ErrExist }
then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for
an example in the standard library. An Is method should only shallowly
compare err and the target and not call Unwrap on either.
func Join
¶
1.20
func Join(errs ...error) error
Join returns an error that wraps the given errors.
Any nil error values are discarded.
Join returns nil if errs contains no non-nil values.
The error formats as the concatenation of the strings obtained
by calling the Error method of each element of errs, with a newline
between each string.
▸ Example
▾ Example
err1 err2 err is err1 err is err2
func New
¶
func New(text string) error
New returns an error that formats as the given text.
Each call to New returns a distinct error value even if the text is identical.
▸ Example
▾ Example
emit macho dwarf: elf header corrupted
▸ Example (Errorf)
▾ Example (Errorf)
The fmt package’s Errorf function lets us use the package’s formatting
features to create descriptive error messages.
user "bimmler" (id 17) not found
func Unwrap
¶
1.13
func Unwrap(err error) error
Unwrap returns the result of calling the Unwrap method on err, if err’s
type contains an Unwrap method returning error.
Otherwise, Unwrap returns nil.
Unwrap returns nil if the Unwrap method returns []error.
Errors are a language-agnostic part that helps to write code in such a way that no unexpected thing happens. When something occurs which is not supported by any means then an error occurs. Errors help to write clean code that increases the maintainability of the program.
What is an error?
An error is a well developed abstract concept which occurs when an exception happens. That is whenever something unexpected happens an error is thrown. Errors are common in every language which basically means it is a concept in the realm of programming.
Why do we need Error?
Errors are a part of any program. An error tells if something unexpected happens. Errors also help maintain code stability and maintainability. Without errors, the programs we use today will be extremely buggy due to a lack of testing.
Golang has support for errors in a really simple way. Go functions returns errors as a second return value. That is the standard way of implementing and using errors in Go. That means the error can be checked immediately before proceeding to the next steps.
Simple Error Methods
There are multiple methods for creating errors. Here we will discuss the simple ones that can be created without much effort.
1. Using the New function
Golang errors package has a function called New() which can be used to create errors easily. Below it is in action.
package main import ( "fmt" "errors" ) func e(v int) (int, error) { if v == 0 { return 0, errors.New("Zero cannot be used") } else { return 2*v, nil } } func main() { v, err := e(0) if err != nil { fmt.Println(err, v) // Zero cannot be used 0 } }
2. Using the Errorf function
The fmt package has an Errorf() method that allows formatted errors as shown below.
fmt.Errorf("Error: Zero not allowed! %v", v) // Error: Zero not allowed! 0
Checking for an Error
To check for an error we simply get the second value of the function and then check the value with the nil. Since the zero value of an error is nil. So, we check if an error is a nil. If it is then no error has occurred and all other cases the error has occurred.
package main import ( "fmt" "errors" ) func e(v int) (int, error) { return 42, errors.New("42 is unexpected!") } func main() { _, err := e(0) if err != nil { // check error here fmt.Println(err) // 42 is unexpected! } }
Panic and recover
Panic occurs when an unexpected wrong thing happens. It stops the function execution. Recover is the opposite of it. It allows us to recover the execution from stopping. Below shown code illustrates the concept.
package main import ( "fmt" ) func f(s string) { panic(s) // throws panic } func main() { // defer makes the function run at the end defer func() { // recovers panic if e := recover(); e != nil { fmt.Println("Recovered from panic") } }() f("Panic occurs!!!") // throws panic // output: // Recovered from panic }
Creating custom errors
As we have seen earlier the function errors.New() and fmt.Errorf() both can be used to create new errors. But there is another way we can do that. And that is implementing the error interface.
type CustomError struct { data string } func (e *CustomError) Error() string { return fmt.Sprintf("Error occured due to... %s", e.data) }
Returning error alongside values
Returning errors are pretty easy in Go. Go supports multiple return values. So we can return any value and error both at the same time and then check the error. Here is a way to do that.
import ( "fmt" "errors" ) func returnError() (int, error) { // declare return type here return 42, errors.New("Error occured!") // return it here } func main() { v, e := returnError() if e != nil { fmt.Println(e, v) // Error occured! 42 } }
Ignoring errors in Golang
Go has the skip (-) operator which allows skipping returned errors at all. Simply using the skip operator helps here.
package main import ( "fmt" "errors" ) func returnError() (int, error) { // declare return type here return 42, errors.New("Error occured!") // return it here } func main() { v, _ := returnError() // skip error with skip operator fmt.Println(v) // 42 }
Recently, I was working on a project which required to handle custom errors in golang. unlike other language, golang does explicit error checking.
An error is just a value that a function can return if something unexpected happened.
In golang, errors are values which return errors as normal return value then we handle errors like if err != nil compare to other conventional try/catch method in another language.
Error in Golang
type error interface {
Error() string
}
Enter fullscreen mode
Exit fullscreen mode
In Go, The error built-in interface type is the conventional interface for representing an error condition, with the nil value representing no error.
It has single Error() method which returns the error message as a string. By implementing this method, we can transform any type we define into an error of our own.
package main
import (
"io/ioutil"
"log"
)
func getFileContent(filename string) ([]byte, error) {
content, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return content, nil
}
func main() {
content, err := getFileContent("filename.txt")
if err != nil {
log.SetFlags(0)
log.Fatal(err)
}
log.Println(string(content))
}
Enter fullscreen mode
Exit fullscreen mode
An idiomatic way to handle errors in Go is to return it as the last return value of the function and check for the nil condition.
Creating errors in go
error is an interface type
Golang provides two ways to create errors in standard library using errors.New and fmt.ErrorF.
errors.New
errors.New("new error")
Enter fullscreen mode
Exit fullscreen mode
Go provides the built-in errors package which exports the New function. This function expects an error text message and returns an error.
The returned error can be treated as a string by either accessing err.Error(), or using any of the fmt package functions. Error() method is automatically called by Golang when printing the error using methods like fmt.Println.
var (
ErrNotFound1 = errors.New("not found")
ErrNotFound2 = errors.New("not found")
)
func main() {
fmt.Printf("is identical? : %t", ErrNotFound1 == ErrNotFound2)
}
// Output:
// is identical? : false
Enter fullscreen mode
Exit fullscreen mode
Each call to New, returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
Enter fullscreen mode
Exit fullscreen mode
Internally, errors.New creates and returns a pointer to errors.errorString struct invoked with the string passed which implements the error interface.
fmt.ErrorF
number := 100
zero := 0
fmt.Errorf("math: %d cannot be divided by %d", number, zero)
Enter fullscreen mode
Exit fullscreen mode
fmt.Errorf provides ability to format your error message using format specifier and returns the string as a
value that satisfies error interface.
Custom errors with data in go
As mentioned above, error is an interface type.
Hence, you can create your own error type by implementing the Error() function defined in the error interface on your struct.
So, let’s create our first custom error by implementing error interface.
package main
import (
"fmt"
"os"
)
type MyError struct {
Code int
Msg string
}
func (m *MyError) Error() string {
return fmt.Sprintf("%s: %d", m.Msg, m.Code)
}
func sayHello(name string) (string, error) {
if name == "" {
return "", &MyError{Code: 2002, Msg: "no name passed"}
}
return fmt.Sprintf("Hello, %s", name), nil
}
func main() {
s, err := sayHello("")
if err != nil {
log.SetFlags(0)
log.Fatal("unexpected error is ", err)
}
fmt.Println(s)
}
Enter fullscreen mode
Exit fullscreen mode
You’ll see the following output:
unexpected error is no name passed: 2002
exit status 1
Enter fullscreen mode
Exit fullscreen mode
In above example, you are creating a custom error using a struct type MyError by implementing Error() function of error interface.
Error wrapping
Golang also allows errors to wrap other errors which is useful when you want to provide additional context to your error messages like providing specific information or more details about the error location in your code.
You can create wrapped errors either with fmt.Errorf or by implementing a custom type. A simple way to create wrapped errors is to call fmt.Errorf with our error which we want to wrap using the %w verb
package main
import (
"fmt"
"io/ioutil"
"log"
)
func getFileContent(filename string) ([]byte, error) {
content, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("error reading file %s: %w", filename, err)
}
return content, nil
}
func main() {
content, err := getFileContent("filename.txt")
if err != nil {
log.SetFlags(0)
log.Fatal(err)
}
log.Println(string(content))
}
Enter fullscreen mode
Exit fullscreen mode
You’ll see the following output:
error reading file filename.txt: open filename.txt: no such file or directory
exit status 1
Enter fullscreen mode
Exit fullscreen mode
Examining errors with Is and As
errors.Is
Unwrap, Is and As functions work on errors that may wrap other errors. An error wraps another error if its type has the method Unwrap() which returns a non-nil error.
errors.Is unwraps its first argument sequentially looking for an error that matches the second and returns boolean true if it finds one.
simple equality checks:
if errors.Is(err, ErrNoNamePassed)
Enter fullscreen mode
Exit fullscreen mode
is preferable to
if err == ErrNoNamePassed
Enter fullscreen mode
Exit fullscreen mode
because the former will succeed if err wraps ErrNoNamePassed.
package main
import (
"errors"
"fmt"
"log"
)
type MyError struct {
Code int
Msg string
}
func (m *MyError) Error() string {
return fmt.Sprintf("%s: %d", m.Msg, m.Code)
}
func main() {
e1 := &MyError{Code: 501, Msg: "new error"}
// wrapping e1 with e2
e2 := fmt.Errorf("E2: %w", e1)
// wrapping e2 with e3
e3 := fmt.Errorf("E3: %w", e2)
fmt.Println(e1) // prints "new error: 501"
fmt.Println(e2) // prints "E2: new error: 501"
fmt.Println(e3) // prints "E3: E2: new error: 501"
fmt.Println(errors.Unwrap(e1)) // prints <nil>
fmt.Println(errors.Unwrap(e2)) // prints "new error: 501"
fmt.Println(errors.Unwrap(e3)) // prints E2: new error: 501
// errors.Is function compares an error to a value.
if errors.Is(e3, e1) {
log.SetFlags(0)
log.Fatal(e3)
}
}
Enter fullscreen mode
Exit fullscreen mode
We’ll see the following output:
new error: 501
E2: new error: 501
E3: E2: new error: 501
<nil>
new error: 501
E2: new error: 501
E3: E2: new error: 501
exit status 1
Enter fullscreen mode
Exit fullscreen mode
errors.As
errors.As unwraps its first argument sequentially looking for an error that can be assigned to its second argument, which must be a pointer. If it succeeds, it performs the assignment and returns true. Otherwise, it returns false.
var e *MyError
if errors.As(err, &e) {
fmt.Println(e.code)
}
Enter fullscreen mode
Exit fullscreen mode
is preferable to
if e, ok := err.(*MyError); ok {
fmt.Println(e)
}
Enter fullscreen mode
Exit fullscreen mode
because the former will succeed if err wraps an *MyError
package main
import (
"errors"
"fmt"
"log"
)
type MyError struct {
Code int
Msg string
}
func (m *MyError) Error() string {
return fmt.Sprintf("%s: %d", m.Msg, m.Code)
}
func main() {
e1 := &MyError{Code: 501, Msg: "new error"}
e2 := fmt.Errorf("E2: %w", e1)
// errors.As function tests whether an error is a specific type.
var e *MyError
if errors.As(e2, &e) {
log.SetFlags(0)
log.Fatal(e2)
}
}
Enter fullscreen mode
Exit fullscreen mode
We’ll see the following output:
E2: new error: 501
exit status 1
Enter fullscreen mode
Exit fullscreen mode
Reference
- Standard library error package https://golang.org/pkg/errors/
- The Go Blog — Working with Errors in Go 1.13
- Error handling in Golang
Conclusion
I hope this article will help you to understand the basics of error handling in Go.
If you have found this useful, please consider recommending and sharing it with other fellow developers and if you have any questions or suggestions, feel free to add a comment or contact me on twitter.