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
does not have an Exception
handling model as most of the main stream
languages. However, it uses the error interface type as the return type for any
error that is going to be returned from a function or method:
type error interface {
Error() string
}
It is an interface type. An error variable represents any value
that can describe itself as a string. The most commonly-used error
implementation is in the errors package.
It can be instaciated in the following way:
func DivideBy(divider float64) (float64, error) {
if divider <= 0 {
return 0, errors.New("Divider cannot be zero or negative number.")
}
// implementation
}
The errors.New
functions constructs an exported type errorString
that
implements the Error
interface:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package errors implements functions to manipulate errors.
package errors
// New returns an error that formats as the given text.
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
}
It is pretty straighforward to implement your own error type that has additional
data.
The error model in Golang
does not provide a way to find out, which function
returned the error. We should be aware and log the errors very carefully in
order to understand where this error occurred.
Fortunately, the Golang
runtime provides a set of functions that we can use to
generate a stacktrace that we can trace down easily.
In the following paragraphs, we will explore the Planatir
stacktrace package that does this for
us.
Getting Started
The package captures a strategic places along the call stack and attaches relevant
contextual information like messages and variables. It is keeping stack traces
compact and maximally useful.
Installation
In order to use the package, we should install it first by using the well known
go get
command:
$ go get github.com/palantir/stacktrace
Usage
The package provides a various functions to propagate and generate that
contextual information.
Error propagation
stacktrace.Propagate
function replaces the usage of fmt.Errorf
function. It
wraps an error to include line number information. This is going to be your
most common stacktrace call.
db, err := sql.Open("postgres", conninfo)
if err != nil {
return stacktrace.Propagate(err, "Failed to connect %v", conninfo)
}
Creating errors
stacktrace.NewError
creates a new error that includes line number information:
if amount <= 0 {
return stacktrace.NewError("Expected amount %v to be positive number", arg)
}
Error Codes
Sometimes it’s useful to propagate an error code while unwinding the
stack. For instance, a RESTful API may use the error code to set the HTTP status
code. The type stacktrace.ErrorCode
is used to name the set of error codes
relevant to your application:
const (
ConnectionTimeout = stacktrace.ErrorCode(iota)
ConnectionLost
)
The value stacktrace.NoCode
is equal to math.MaxUint16
, so avoid using
that. NoCode
is the default value of errors that does not have explicitly set
error code.
You can use stacktrace.PropagateWithCode
and stacktrace.NewErrorWithCode
to
instaciated an error that has specific code:
db, err := sql.Open("postgres", conninfo)
if err != nil {
return stacktrace.PropagateWithCode(err, ConnectionTimeout, "Failed to connect %v", conninfo)
}
You can extract the error code from the error by using stacktrace.GetCode
function:
data, err := fetch()
if err != nil {
code := stacktrace.GetCode(err)
if code == ConnectionTimeout {
return nil
}
}
Verdict
The stacktrace
package is very pleasant and easy to use. It comforms the Golang
idiomatic way of handling errors and provides us with additional contextual information
about the file and the line where the error occurred.
Errors are a core part of almost every programming language, and how we handle them is a critical part of software development. One of the things that I really enjoy about programming in Go is the implementation of errors and how they are treated: Effective without having unnecessary complexity. This blog post will dive into what errors are and how they can be wrapped (and unwrapped).
What are errors?
Let’s start from the beginning. It’s common to see an error
getting returned and handled from a function:
1 2 3 |
func myFunction() error { // ... } |
But what exactly is an error
? It is one of the simplest interfaces defined in Go (source code reference):
1 2 3 |
type error interface { Error() string } |
It has a single function Error
that takes no parameters and returns a string. That’s it! That’s all there is to implementing the error
interface. We’ll see later on how we can implement error
to create our own custom error types.
Creating errors
Most of the time you’ll rely on creating errors through one of two ways:
1 |
fmt.Errorf("error doing something") |
Or:
1 |
errors.New("error doing something") |
The former is used when you want to use formatting with the typical fmt verbs. If you aren’t wrapping an error (more on this below) then fmt.Errorf
effectively makes a call to errors.New
(source code reference). So if you’re not wrapping an error or using any additional formatting then it’s a personal preference.
What do these non-wrapped errors look like? Breaking into the debugger we can analyze them:
1 2 3 |
(dlv) p err1 error(*errors.errorString) *{ s: "error doing something",} |
The concrete type is *errors.errorString
. Let’s take a look at this Go struct in the errors
package (source code reference):
1 2 3 4 5 6 7 |
type errorString struct { s string } func (e *errorString) Error() string { return e.s } |
errorString
is a simple implementation having a single string
field and because this implements the error
interface it defines the Error
function, which just returns the struct’s string.
Creating custom error types
What if we want to create custom errors with certain pieces of information? Now that we understand what an error actually is (by implementing the error
interface) we can define our own:
1 2 3 4 5 6 7 8 |
type myCustomError struct { errorMessage string someRandomValue int } func (e *myCustomError) Error() string { return fmt.Sprintf("Message: %s - Random value: %d", e.errorMessage, e.someRandomValue) } |
And we can use them just like any other error
in Go:
1 2 3 4 5 6 7 8 9 10 11 |
func someFunction() error { return &myCustomError{ errorMessage: "hello world", someRandomValue: 13, } } func main() { err := someFunction() fmt.Printf("%v", err) } |
The output of running this code is expected:
1 |
Message: hello world - Random value: 13 |
Error wrapping
In Go it is common to return errors and then keep bubbling that up until it is handled properly (exiting, logging, etc.). Consider this example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func doAnotherThing() error { return errors.New("error doing another thing") } func doSomething() error { err := doAnotherThing() return fmt.Errorf("error doing something: %v", err) } func main() { err := doSomething() fmt.Println(err) } |
main
makes a call to doSomething
, which calls doAnotherThing
and takes its error.
Note: It’s common to have error handling with if err != nil ...
but I wanted to keep this example as small as possible.
Typically you want to preserve the context of your inner errors (in this case “error doing another thing”) so you might try to do a superficial wrap with fmt.Errorf
and the %v
verb. In fact, the output seems reasonable:
1 |
error doing something: error doing another thing |
But outside of wrapping the error messages, we’ve lost the inner errors themselves effectively. If we were to analyze err
in main
, we’d see this:
1 2 3 |
(dlv) p err error(*errors.errorString) *{ s: "error doing something: error doing another thing",} |
Most of the time that is typically fine. A superficially wrapper error is ok for logging and troubleshooting. But what happens if you need to programmatically test for a particular error or treat an error as a custom one? With the above approach, that is extremely complicated and error-prone.
The solution to that challenge is by wrapping your errors. To wrap your errors you would use fmt.Errorf
with the %w
verb. Let’s modify the single line of code in the above example:
1 |
return fmt.Errorf("error doing something: %w", err) |
Now let’s inspect the returned error in main:
1 2 3 4 5 |
(dlv) p err error(*fmt.wrapError) *{ msg: "error doing something: error doing another thing", err: error(*errors.errorString) *{ s: "error doing another thing",},} |
We’re no longer getting the type *errors.errorString
. Now we have the type *fmt.wrapError
. Let’s take a look at how Go defines wrapError
(source code reference):
1 2 3 4 5 6 7 8 9 10 11 12 |
type wrapError struct { msg string err error } func (e *wrapError) Error() string { return e.msg } func (e *wrapError) Unwrap() error { return e.err } |
This adds a couple of new things:
err
field with the typeerror
(this will be the inner/wrapped error)Unwrap
method that gives us access to the inner/wrapped error
This extra wiring gives us a lot of powerful capabilities when dealing with wrapped errors.
Error equality
One of the scenarios that error wrapping unlocks is a really elegant way to test if an error or any inner/wrapped errors are a particular error. We can do that with the errors.Is
function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var errAnotherThing = errors.New("error doing another thing") func doAnotherThing() error { return errAnotherThing } func doSomething() error { err := doAnotherThing() return fmt.Errorf("error doing something: %w", err) } func main() { err := doSomething() if errors.Is(err, errAnotherThing) { fmt.Println("Found error!") } fmt.Println(err) } |
I changed the code of doAnotherThing
to return a particular error (errAnotherThing
). Even though this error gets wrapped in doSomething
, we’re still able to concisely test if the returned error is or wraps errAnotherThing
with errors.Is
.
errors.Is
essentially just loops through the different layers of the error and unwraps, testing to see if it is equal to the target error (source code reference).
Specific error handling
Another scenario is if you have a particular type of error that you want to handle, even if it is wrapped. Using a variation of an earlier example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
type myCustomError struct { errorMessage string someRandomValue int } func (e *myCustomError) Error() string { return fmt.Sprintf("Message: %s - Random value: %d", e.errorMessage, e.someRandomValue) } func doAnotherThing() error { return &myCustomError{ errorMessage: "hello world", someRandomValue: 13, } } func doSomething() error { err := doAnotherThing() return fmt.Errorf("error doing something: %w", err) } func main() { err := doSomething() var customError *myCustomError if errors.As(err, &customError) { fmt.Printf("Custom error random value: %dn", customError.someRandomValue) } fmt.Println(err) } |
This allows us to handle err
in main if it (or any wrapped errors) have a concrete type of *myCustomError
. The output of running this code:
1 2 |
Custom error random value: 13 error doing something: Message: hello world - Random value: 13 |
Summary
Understanding how errors and error wrapping in Go can go a really long way in implementing them in the best possible way. Using them “the Go way” can lead to code that is easier to maintain and troubleshoot. Enjoy!
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.