To check the TYPE of the error, use errors.As
As
finds the first error in err’s chain that matches target […] An error matches target if the error’s concrete value is assignable to the value pointed to by target
Of course type identity is a condition for assignability.
So it would look like:
target := &model.ModelMissingError{}
if errors.As(err, &target) {
fmt.Println(target) // no model found for id
}
Pay attention to the two uses of &
in the example above. This is because:
As
panics if target is not a non-nil pointer to either a type that implements error, or to any interface type.
In your case, you declared Error() string
method on the pointer receiver, therefore «a pointer to the type that implements the error
interface» to satisfy As
is **ModelMissingError
. So you need to address twice.
The other method errors.Is
checks for value equality.
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.
This is useful for example in case of fixed error values, e.g. errors declared as var
or const
like the standard library io.EOF
. As an example:
var ErrModelMissing = errors.New("no model found for id")
func foo() {
err := bar()
if errors.Is(err, ErrModelMissing) {
fmt.Println(err) // no model found for id
}
}
Consider that the usefulness of Go 1.13 errors.As
and errors.Is
lies in error unwrapping. If you inspect the error at the top of an arbitrarily long call stack, you must remember that the original error may become wrapped into other errors while being bubbled up. Then directly checking for equality or type assignability is not enough.
err1 := fmt.Errorf("wrapped: %w", &ModelMissingError{})
target := &ModelMissingError{}
fmt.Println(errors.As(err1, &target)) // true
err2 := fmt.Errorf("wrapped: %w", FixedError)
fmt.Println(errors.Is(err2, FixedError)) // true
fmt.Println(err2 == FixedError) // false
Additionally, the package github.com/pkg/errors
is compatible with errors.As
and errors.Is
:
// import pkgerr "github.com/pkg/errors"
err3 := pkgerr.Wrap(pkgerr.Wrap(&ModelMissingError{}, "wrapped 1"), "wrapped 2")
fmt.Println(errors.As(err3, &target)) // true
Playground: https://play.golang.org/p/FEzggdBLCqq
Naturally, if you know for sure that the error is not wrapped, a good old type assertion works just as fine:
if myerr, ok := err.(*model.ModelMissingError); ok {
// handle myerr
}
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 ▸
Variables
var errorType = reflectlite.TypeOf((*error)(nil)).Elem()
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.
type errorString
¶
errorString is a trivial implementation of error.
type errorString struct { s string }
func (*errorString) Error
¶
func (e *errorString) Error() string
type joinError
¶
type joinError struct { errs []error }
func (*joinError) Error
¶
func (e *joinError) Error() string
func (*joinError) Unwrap
¶
func (e *joinError) Unwrap() []error
Since Go 1.13, we have new helpful ways to find out the type of error, even if we use error wrapping. If we want to check if a given error matches another specific error, we need to use Is()
function from the errors
package. If we are interested in whether the error is of a given type, we should call the As()
function.
Is()
function
In the example below, we can see that the function validateInput
returns an error for badInput
. This error is ErrBadInput
wrapped in an error created by fmt.Errorf()
. Using the Is(err, target error) bool
function, we can detect the ErrBadInput
even if it is wrapped since this function checks if any error in the chain of wrapped errors matches the target. Therefore, this form should be preferable to comparison if err == ErrBadInput
.
package main
import (
"errors"
"fmt"
)
const badInput = "abc"
var ErrBadInput = errors.New("bad input")
func validateInput(input string) error {
if input == badInput {
return fmt.Errorf("validateInput: %w", ErrBadInput)
}
return nil
}
func main() {
input := badInput
err := validateInput(input)
if errors.Is(err, ErrBadInput) {
fmt.Println("bad input error")
}
}
Output:
As()
function
Similar to Is()
, the As(err error, target interface{}) bool
checks if any error in the chain of wrapped errors matches the target. The difference is that this function checks whether the error has a specific type, unlike the Is()
, which examines if it is a particular error object. Because As
considers the whole chain of errors, it should be preferable to the type assertion if e, ok := err.(*BadInputError); ok
.
target
argument of theAs(err error, target interface{}) bool
function should be a pointer to the error type, which in this case is*BadInputError
package main
import (
"errors"
"fmt"
)
const badInput = "abc"
type BadInputError struct {
input string
}
func (e *BadInputError) Error() string {
return fmt.Sprintf("bad input: %s", e.input)
}
func validateInput(input string) error {
if input == badInput {
return fmt.Errorf("validateInput: %w", &BadInputError{input: input})
}
return nil
}
func main() {
input := badInput
err := validateInput(input)
var badInputErr *BadInputError
if errors.As(err, &badInputErr) {
fmt.Printf("bad input error occured: %sn", badInputErr)
}
}
Output:
bad input error occured: bad input: abc
Welcome to tutorial no. 30 in Golang tutorial series.
What are errors?
Errors indicate any abnormal condition occurring in the program. Let’s say we are trying to open a file and the file does not exist in the file system. This is an abnormal condition and it’s represented as an error.
Errors in Go are plain old values. Just like any other built-in type such as int, float64, … error values can be stored in variables, passed as parameters to functions, returned from functions, and so on.
Errors are represented using the built-in error
type. We will learn more about the error
type later in this tutorial.
Example
Let’s start right away with an example program that tries to open a file that does not exist.
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("/test.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(f.Name(), "opened successfully")
}
Run in playground
In line no. 9 of the program above, we are trying to open the file at path /test.txt
(which will obviously not exist in the playground). The Open function of the os
package has the following signature,
func Open(name string) (*File, error)
If the file has been opened successfully, then the Open function will return the file handler and error will be nil. If there is an error while opening the file, a non-nil error will be returned.
If a function or method returns an error, then by convention it has to be the last value returned from the function. Hence the Open
function returns error
as the last value.
The idiomatic way of handling errors in Go is to compare the returned error to nil
. A nil value indicates that no error has occurred and a non-nil value indicates the presence of an error. In our case, we check whether the error is not nil
in line no. 10. If it is not nil
, we simply print the error and return from the main function.
Running this program will print
open /test.txt: No such file or directory
Perfect 😃. We get an error stating that the file does not exist.
Error type representation
Let’s dig a little deeper and see how the built in error
type is defined. error is an interface type with the following definition,
type error interface {
Error() string
}
It contains a single method with the signature Error() string
. Any type which implements this interface can be used as an error. This method provides the description of the error.
When printing the error, fmt.Println
function calls the Error() string
method internally to get the description of the error. This is how the error description was printed in line no. 11 of the above sample program.
Now that we know error
is an interface type, let’s see how we can extract more information about an error.
In the example we saw above, we have just printed the description of the error. What if we wanted the actual path of the file which caused the error. One possible way to get this is to parse the error string. This was the output of our program,
open /test.txt: No such file or directory
We can parse this error message and get the file path «/test.txt» of the file which caused the error, but this is a dirty way of doing it. The error description can change at any time in newer versions of Go and our code will break.
Is there a better way to get the file name 🤔? The answer is yes, it can be done and the Go standard library uses different ways to provide more information about errors. Let’s look at them one by one.
1. Converting the error to the underlying type and retrieving more information from the struct fields
If you read the documentation of the Open function carefully, you can see that it returns an error of type *PathError.
PathError is a struct type and its implementation in the standard library is as follows,
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
In case you are interested to know where the above source code exists, it can be found here https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/io/fs/fs.go;l=250
From the above code, you can understand that *PathError
implements the error interface
by declaring the Error() string
method. This method concatenates the operation, path, and the actual error and returns it. Thus we got the error message,
open /test.txt: No such file or directory
The Path
field of PathError
struct contains the path of the file which caused the error.
We can use the As function from errors package to convert the error to it’s underlying type. The As
function’s description talks about error chain. Please ignore it for now. We will understand how error chain and wrapping works in a separate tutorial.
A simple description of As
is that it tries to convert the error to a error type and returns either true or false indicating whether the conversion is successful or not.
A program will make things clear. Let’s modify the program we wrote above and print the path using the As
function.
package main
import (
"errors"
"fmt"
"os"
)
func main() {
f, err := os.Open("test.txt")
if err != nil {
var pErr *os.PathError
if errors.As(err, &pErr) {
fmt.Println("Failed to open file at path", pErr.Path)
return
}
fmt.Println("Generic error", err)
return
}
fmt.Println(f.Name(), "opened successfully")
}
Run in Playground
In the above program, we first check whether the error is not nil
in line no. 11 and then we use the As
function in line no. 13 to convert err
to *os.PathError
. If the conversion is successful, As
will return true
. Then we print the path using pErr.Path
in line no. 14.
If you are wondering why pErr
is a pointer, the reason is, the error interface is implemented by the pointer of PathError
and hence pErr
is a pointer. The below code shows that *PathError
implements the error interface.
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
The As
function requires the second argument to be a pointer to the type that implements the error. Hence we pass &perr
.
This program outputs,
Failed to open file at path test.txt
In case the underlying error is not of type *os.PathError
, the control will reach line no. 17 and a generic error message will be printed.
Great 😃. We have successfully used the As
function to get the file path from the error.
2. Retrieving more information using methods
The second way to get more information from the error is to find out the underlying type and get more information by calling methods on the struct type.
Let’s understand this better by means of an example.
The DNSError struct type in the standard library is defined as follows,
type DNSError struct {
...
}
func (e *DNSError) Error() string {
...
}
func (e *DNSError) Timeout() bool {
...
}
func (e *DNSError) Temporary() bool {
...
}
The DNSError
struct has two methods Timeout() bool
and Temporary() bool
which return a boolean value that indicates whether the error is because of a timeout or is it a temporary one.
Let’s write a program that converts the error to *DNSError
type and calls the above mentioned methods to determine whether the error is temporary or due to timeout.
package main
import (
"errors"
"fmt"
"net"
)
func main() {
addr, err := net.LookupHost("golangbot123.com")
if err != nil {
var dnsErr *net.DNSError
if errors.As(err, &dnsErr) {
if dnsErr.Timeout() {
fmt.Println("operation timed out")
return
}
if dnsErr.Temporary() {
fmt.Println("temporary error")
return
}
fmt.Println("Generic DNS error", err)
return
}
fmt.Println("Generic error", err)
return
}
fmt.Println(addr)
}
Note: DNS lookups do not work in the playground. Please run this program in your local machine.
In the program above, in line no. 9, we are trying to get the IP address of an invalid domain name golangbot123.com
. In line no. 13 we get the underlying value of the error by using the As
function and converting it to *net.DNSError
. Then we check whether the error is due to timeout or is temporary in line nos. 14 and 18 respectively.
In our case, the error is neither temporary nor due to timeout and hence the program will print,
Generic DNS error lookup golangbot123.com: no such host
If the error was temporary or due to a timeout, then the corresponding if statement would have executed and we can handle it appropriately.
3. Direct comparison
The third way to get more details about an error is the direct comparison with a variable of type error
. Let’s understand this by means of an example.
The Glob function of the filepath
package is used to return the names of all files that matches a pattern. This function returns an error ErrBadPattern
when the pattern is malformed.
ErrBadPattern is defined in the filepath
package as a global variable.
var ErrBadPattern = errors.New("syntax error in pattern")
errors.New() is used to create a new error. We will discuss this in detail in the next tutorial.
ErrBadPattern is returned by the Glob function when the pattern is malformed.
Let’s write a small program to check for this error.
package main
import (
"errors"
"fmt"
"path/filepath"
)
func main() {
files, err := filepath.Glob("[")
if err != nil {
if errors.Is(err, filepath.ErrBadPattern) {
fmt.Println("Bad pattern error:", err)
return
}
fmt.Println("Generic error:", err)
return
}
fmt.Println("matched files", files)
}
Run in playground
In the program above we search for files of pattern [
which is a malformed pattern. We check whether the error is not nil. To get more information about the error, we directly compare it to filepath.ErrBadPattern
in line. no 11 using the Is function. Similar to As
, the Is
function works on an error chain. We will learn more about this in our next tutorial.
For the purposes of this tutorial, the Is
function can be thought of as returning true
if both the errors passed to it are the same.
The Is
returns true in line no. 12 since the error is due to a malformed pattern. This program will print,
Bad pattern error: syntax error in pattern
The standard library uses any of the above-mentioned ways to provide more information about an error. We will use these ways in the next tutorial to create our own custom errors.
Do not ignore errors
Never ever ignore an error. Ignoring errors is inviting for trouble. Let me rewrite the example which lists the name of all files that match a pattern ignoring errors.
package main
import (
"fmt"
"path/filepath"
)
func main() {
files, _ := filepath.Glob("[")
fmt.Println("matched files", files)
}
Run in playground
We already know from the previous example that the pattern is invalid. I have ignored the error returned by the Glob
function by using the _
blank identifier in line no. 9. I simply print the matched files in line no. 10. This program will print,
matched files []
Since we ignored the error, the output seems as if no files have matched the pattern but actually the pattern itself is malformed. So never ignore errors.
This brings us to the end of this tutorial.
In this tutorial, we discussed how to handle errors that occur in our program and also how to inspect the errors to get more information from them. A quick recap of what we discussed in this tutorial,
- What are errors?
- Error representation
- Various ways of extracting more information from errors
- Do not ignore errors
In the next tutorial, we will create our own custom errors and also add more context to our custom errors.
Thanks for reading. Please leave your comments and feedback.
Like my tutorials? Please support the content.
Next tutorial — Custom Errors
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.
Александр Тихоненко
Ведущий разработчик трайба «Автоматизация бизнес-процессов» МТС Диджитал
Механизм обработки ошибок в Go отличается от обработки исключений в большинстве языков программирования, ведь в Golang ошибки исключениями не являются. Если говорить в целом, то ошибка в Go — это возвращаемое значение с типомerror
, которое демонстрирует сбой. А с точки зрения кода — интерфейс. В качестве ошибки может выступать любой объект, который этому интерфейсу удовлетворяет.
Выглядит это так:
type error interface {
Error() string
}
В данной статье мы рассмотрим наиболее популярные способы работы с ошибками в Golang.
- Как обрабатывать ошибки в Go?
- Создание ошибок
- Оборачивание ошибок
- Проверка типов с Is и As
- Сторонние пакеты по работе с ошибками в Go
- Defer, panic and recover
- После изложенного
Чтобы обработать ошибку в Golang, необходимо сперва вернуть из функции переменную с объявленным типом error
и проверить её на nil
:
if err != nil {
return err
}
Если метод возвращает ошибку, значит, потенциально в его работе может возникнуть проблема, которую нужно обработать. В качестве реализации обработчика может выступать логирование ошибки или более сложные сценарии. Например, переоткрытие установленного сетевого соединения, повторный вызов метода и тому подобные операции.
Если метод возвращает разные типы ошибок, то их нужно проверять отдельно. То есть сначала происходит определение ошибки, а потом для каждого типа пишется свой обработчик.
В Go ошибки возвращаются и проверяются явно. Разработчик сам определяет, какие ошибки метод может вернуть, и реализовать их обработку на вызывающей стороне.
Создание ошибок
Перед тем как обработать ошибку, нужно её создать. В стандартной библиотеке для этого есть две встроенные функции — обе позволяют указывать и отображать сообщение об ошибке:
errors.New
fmt.Errorf
Метод errors.New()
создаёт ошибку, принимая в качестве параметра текстовое сообщение.
package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("emit macho dwarf: elf header corrupted")
fmt.Print(err)
}
С помощью метода fmt.Errorf
можно добавить дополнительную информацию об ошибке. Данные будут храниться внутри одной конкретной строки.
package main
import (
"fmt"
)
func main() {
const name, id = "bueller", 17
err := fmt.Errorf("user %q (id %d) not found", name, id)
fmt.Print(err)
}
Такой способ подходит, если эта дополнительная информация нужна только для логирования на вызывающей стороне. Если же с ней предстоит работать, можно воспользоваться другими механизмами.
Оборачивание ошибок
Поскольку Error
— это интерфейс, можно создать удовлетворяющую ему структуру с собственными полями. Тогда на вызывающей стороне этими самыми полями можно будет оперировать.
package main
import (
"fmt"
)
type NotFoundError struct {
UserId int
}
func (err NotFoundError) Error() string {
return fmt.Sprintf("user with id %d not found", err.UserId)
}
func SearchUser(id int) error {
// some logic for search
// ...
// if not found
var err NotFoundError
err.UserId = id
return err
}
func main() {
const id = 17
err := SearchUser(id)
if err != nil {
fmt.Println(err)
//type error checking
notFoundErr, ok := err.(NotFoundError)
if ok {
fmt.Println(notFoundErr.UserId)
}
}
}
Представим другую ситуацию. У нас есть метод, который вызывает внутри себя ещё один метод. В каждом из них проверяется своя ошибка. Иногда требуется в метод верхнего уровня передать сразу обе эти ошибки.
В Go есть соглашение о том, что ошибка, которая содержит внутри себя другую ошибку, может реализовать метод Unwrap
, который будет возвращать исходную ошибку.
Также для оборачивания ошибок в fmt.Errorf
есть плейсхолдер %w
, который и позволяет произвести такую упаковку.:
package main
import (
"errors"
"fmt"
"os"
)
func main() {
err := openFile("non-existing")
if err != nil {
fmt.Println(err.Error())
// get internal error
fmt.Println(errors.Unwrap(err))
}
}
func openFile(filename string) error {
if _, err := os.Open(filename); err != nil {
return fmt.Errorf("error opening %s: %w", filename, err)
}
return nil
}
Проверка типов с Is и As
В Go 1.13 в пакете Errors появились две функции, которые позволяют определить тип ошибки — чтобы написать тот или иной обработчик:
errors.Is
errors.As
Метод errors.Is
, по сути, сравнивает текущую ошибку с заранее заданным значением ошибки:
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)
}
}
}
Если это будет та же самая ошибка, то функция вернёт true
, если нет — false
.
errors.As
проверяет, относится ли ошибка к конкретному типу (раньше надо было явно приводить тип ошибки к тому типу, который хотим проверить):
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)
}
}
}
Помимо прочего, эти методы удобны тем, что упрощают работу с упакованными ошибками, позволяя проверить каждую из них за один вызов.
Сторонние пакеты по работе с ошибками в Go
Помимо стандартного пакета Go, есть различные внешние библиотеки, которые расширяют функционал. При принятии решения об их использовании следует отталкиваться от задачи — использование может привести к падению производительности.
В качестве примера можно посмотреть на пакет pkg/errors
. Одной из его способностей является логирование stack trace:
package main
import (
"fmt"
"github.com/pkg/errors"
)
func main() {
err := errors.Errorf("whoops: %s", "foo")
fmt.Printf("%+v", err)
}
// Example output:
// whoops: foo
// github.com/pkg/errors_test.ExampleErrorf
// /home/dfc/src/github.com/pkg/errors/example_test.go:101
// testing.runExample
// /home/dfc/go/src/testing/example.go:114
// testing.RunExamples
// /home/dfc/go/src/testing/example.go:38
// testing.(*M).Run
// /home/dfc/go/src/testing/testing.go:744
// main.main
// /github.com/pkg/errors/_test/_testmain.go:102
// runtime.main
// /home/dfc/go/src/runtime/proc.go:183
// runtime.goexit
// /home/dfc/go/src/runtime/asm_amd64.s:2059
Defer, panic and recover
Помимо ошибок, о которых позаботился разработчик, в Go существуют аварии (похожи на исключительные ситуации, например, в Java). По сути, это те ошибки, которые разработчик не предусмотрел.
При возникновении таких ошибок Go останавливает выполнение программы и начинает раскручивать стек вызовов до тех пор, пока не завершит работу приложения или не найдёт функцию обработки аварии.
Для работы с такими ошибками существует механизм «defer, panic, recover»
Defer
Defer
помещает все вызовы функции в стек приложения. При этом отложенные функции выполняются в обратном порядке — независимо от того, вызвана паника или нет. Это бывает полезно при очистке ресурсов:
package main
import (
"fmt"
"os"
)
func main() {
f := createFile("/tmp/defer.txt")
defer closeFile(f)
writeFile(f)
}
func createFile(p string) *os.File {
fmt.Println("creating")
f, err := os.Create(p)
if err != nil {
panic(err)
}
return f
}
func writeFile(f *os.File) {
fmt.Println("writing")
fmt.Fprintln(f, "data")
}
func closeFile(f *os.File) {
fmt.Println("closing")
err := f.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %vn", err)
os.Exit(1)
}
}
Panic
Panic
сигнализирует о том, что код не может решить текущую проблему, и останавливает выполнение приложения. После вызова оператора выполняются все отложенные функции, и программа завершается с сообщением о причине паники и трассировки стека.
Например, Golang будет «паниковать», когда число делится на ноль:
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
Также панику можно вызвать явно с помощью метода panic()
. Обычно его используют на этапе разработки и тестирования кода — а в конечном варианте убирают.
Recover
Эта функция нужна, чтобы вернуть контроль при панике. В таком случае работа приложения не прекращается, а восстанавливается и продолжается в нормальном режиме.
Recover всегда должна вызываться в функции defer
. Чтобы сообщить об ошибке как возвращаемом значении, вы должны вызвать функцию recover в той же горутине, что и паника, получить структуру ошибки из функции восстановления и передать её в переменную:
package main
import (
"errors"
"fmt"
)
func A() {
defer fmt.Println("Then we can't save the earth!")
defer func() {
if x := recover(); x != nil {
fmt.Printf("Panic: %+vn", x)
}
}()
B()
}
func B() {
defer fmt.Println("And if it keeps getting hotter...")
C()
}
func C() {
defer fmt.Println("Turn on the air conditioner...")
Break()
}
func Break() {
defer fmt.Println("If it's more than 30 degrees...")
panic(errors.New("Global Warming!!!"))
}
func main() {
A()
}
После изложенного
Можно ли игнорировать ошибки? В теории — да. Но делать это нежелательно. Во-первых, наличие ошибки позволяет узнать, успешно ли выполнился метод. Во-вторых, если метод возвращает полезное значение и ошибку, то, не проверив её, нельзя утверждать, что полезное значение корректно.
Надеемся, приведённые методы обработки ошибок в Go будут вам полезны. Читайте также статью о 5 главных ошибках Junior-разработчика, чтобы не допускать их в начале своего карьерного пути.