Written by Raphael Ugwu✏️
Unlike conventional methods in other mainstream programming languages such as JavaScript (which uses the try… catch
statement) or Python (with its try… except
block) tackling errors in Go requires a different approach. Why? Because its features for error handling are often misapplied.
In this blog post, we’ll take a look at the best practices that could be used to handle errors in a Go application. A basic understanding of how Go works is all that is required to digest this article – should you feel stuck at some point, it’s okay to take some time and research unfamiliar concepts.
The blank identifier
The blank identifier is an anonymous placeholder. It may be used like any other identifier in a declaration, but it does not introduce a binding. The blank identifier provides a way to ignore left-handed values in an assignment and avoid compiler errors about unused imports and variables in a program. The practice of assigning errors to the blank identifier instead of properly handling them is unsafe as this means you have decided to explicitly ignore the value of the defined function.
result, _ := iterate(x,y)
if value > 0 {
// ensure you check for errors before results.
}
Your reason for probably doing this is that you’re not expecting an error from the function (or whatever error may occur) but this could create cascading effects in your program. The best thing to do is to handle an error whenever you can.
Handling errors through multiple return values
One way to handle errors is to take advantage of the fact that functions in Go support multiple return values. Thus you can pass an error variable alongside the result of the function you’re defining:
func iterate(x, y int) (int, error) {
}
In the code sample above, we have to return the predefined error
variable if we think there’s a chance our function may fail. error
is an interface type declared in Go’s built-in
package and its zero value is nil
.
type error interface {
Error() string
}
Usually, returning an error means there’s a problem and returning nil
means there were no errors:
result, err := iterate(x, y)
if err != nil {
// handle the error appropriately
} else {
// you're good to go
}
Thus whenever the function iterate
is called and err
is not equal to nil
, the error returned should be handled appropriately – an option could be to create an instance of a retry or cleanup mechanism. The only drawback with handling errors this way is that there’s no enforcement from Go’s compiler, you have to decide on how the function you created returns the error. You can define an error struct and place it in the position of the returned values. One way to do this is by using the built-in errorString
struct (you can also find this code at Go’s source code):
package errors
func New(text string) error {
return &errorString {
text
}
}
type errorString struct {
s string
}
func(e * errorString) Error() string {
return e.s
}
In the code sample above, errorString
embeds a string
which is returned by the Error
method. To create a custom error, you’ll have to define your error struct and use method sets to associate a function to your struct:
// Define an error struct
type CustomError struct {
msg string
}
// Create a function Error() string and associate it to the struct.
func(error * CustomError) Error() string {
return error.msg
}
// Then create an error object using MyError struct.
func CustomErrorInstance() error {
return &CustomError {
"File type not supported"
}
}
The newly created custom error can then be restructured to use the built-in error
struct:
import "errors"
func CustomeErrorInstance() error {
return errors.New("File type not supported")
}
One limitation of the built-in error
struct is that it does not come with stack traces. This makes locating where an error occurred very difficult. The error could pass through a number of functions before it gets printed out. To handle this, you could install the pkg/errors
package which provides basic error handling primitives such as stack trace recording, error wrapping, unwrapping, and formatting. To install this package, run this command in your terminal:
go get github.com/pkg/errors
When you need to add stack traces or any other information that makes debugging easier to your errors, use the New
or Errorf
functions to provide errors that record your stack trace. Errorf
implements the fmt.Formatter
interface which lets you format your errors using the fmt
package runes (%s
, %v
, %+v
etc):
import(
"github.com/pkg/errors"
"fmt"
)
func X() error {
return errors.Errorf("Could not write to file")
}
func customError() {
return X()
}
func main() {
fmt.Printf("Error: %+v", customError())
}
To print stack traces instead of a plain error message, you have to use %+v
instead of %v
in the format pattern, and the stack traces will look similar to the code sample below:
Error: Could not write to file
main.X
/Users/raphaelugwu/Go/src/golangProject/error_handling.go:7
main.customError
/Users/raphaelugwu/Go/src/golangProject/error_handling.go:15
main.main
/Users/raphaelugwu/Go/src/golangProject/error_handling.go:19
runtime.main
/usr/local/opt/go/libexec/src/runtime/proc.go:192
runtime.goexit
/usr/local/opt/go/libexec/src/runtime/asm_amd64.s:2471
Defer, panic, and recover
Although Go doesn’t have exceptions, it has a similar kind of mechanism known as “Defer, panic, and recover“. Go’s ideology is that adding exceptions such as the try/catch/finally
statement in JavaScript would result in complex code and encourage programmers to label too many basic errors, such as failing to open a file, as exceptional. You should not use defer/panic/recover
as you would throw/catch/finally
; only in cases of unexpected, unrecoverable failure.
Defer
is a language mechanism that puts your function call into a stack. Each deferred function is executed in reverse order when the host function finishes regardless of whether a panic is called or not. The defer mechanism is very useful for cleaning up resources:
package main
import (
"fmt"
)
func A() {
defer fmt.Println("Keep calm!")
B()
}
func B() {
defer fmt.Println("Else...")
C()
}
func C() {
defer fmt.Println("Turn on the air conditioner...")
D()
}
func D() {
defer fmt.Println("If it's more than 30 degrees...")
}
func main() {
A()
}
This would compile as:
If it's more than 30 degrees...
Turn on the air conditioner...
Else...
Keep calm!
Panic
is a built-in function that stops the normal execution flow. When you call panic
in your code, it means you’ve decided that your caller can’t solve the problem. Thus panic
should only be used in rare cases where it’s not safe for your code or anyone integrating your code to continue at that point. Here’s a code sample depicting how panic
works:
package main
import (
"errors"
"fmt"
)
func A() {
defer fmt.Println("Then we can't save the earth!")
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()
}
The sample above would compile as:
If it's more than 30 degrees...
Turn on the air conditioner...
And if it keeps getting hotter...
Then we can't save the earth!
panic: Global Warming!!!
goroutine 1 [running]:
main.Break()
/tmp/sandbox186240156/prog.go:22 +0xe0
main.C()
/tmp/sandbox186240156/prog.go:18 +0xa0
main.B()
/tmp/sandbox186240156/prog.go:14 +0xa0
main.A()
/tmp/sandbox186240156/prog.go:10 +0xa0
main.main()
/tmp/sandbox186240156/prog.go:26 +0x20
Program exited: status 2.
As shown above, when panic
is used and not handled, the execution flow stops, all deferred functions are executed in reverse order and stack traces are printed.
You can use the recover
built-in function to handle panic
and return the values passing from a panic call. recover
must always be called in a defer
function else it will return nil
:
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: %+v\n", 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()
}
As can be seen in the code sample above, recover
prevents the entire execution flow from coming to a halt because we threw in a panic
function and the compiler would return:
If it's more than 30 degrees...
Turn on the air conditioner...
And if it keeps getting hotter...
Panic: Global Warming!!!
Then we can't save the earth!
Program exited.
To report an error as a return value, you have to call the recover
function in the same goroutine as the panic
function is called, retrieve an error struct from the recover
function, and pass it to a variable:
package main
import (
"errors"
"fmt"
)
func saveEarth() (err error) {
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
TooLate()
return
}
func TooLate() {
A()
panic(errors.New("Then there's nothing we can do"))
}
func A() {
defer fmt.Println("If it's more than 100 degrees...")
}
func main() {
err := saveEarth()
fmt.Println(err)
}
Every deferred function will be executed after a function call but before a return statement. So, you can set a returned variable before a return statement gets executed. The code sample above would compile as:
If it's more than 100 degrees...
Then there's nothing we can do
Program exited.
Error wrapping
Previously error wrapping in Go was only accessible via using packages such as pkg/errors
. However, with Go’s latest release – version 1.13, support for error wrapping is present. According to the release notes:
An error
e
can wrap another errorw
by providing anUnwrap
method that returnsw
. Bothe
andw
are available to programs, allowinge
to provide additional context tow
or to reinterpret it while still allowing programs to make decisions based onw
.
To create wrapped errors, fmt.Errorf
now has a %w
verb and for inspecting and unwrapping errors, a couple of functions have been added to the error
package:
errors.Unwrap
: This function basically inspects and exposes the underlying errors in a program. It returns the result of calling the Unwrap
method on Err
. If Err’s type contains an Unwrap
method returning an error. Otherwise, Unwrap
returns nil
.
package errors
type Wrapper interface{
Unwrap() error
}
Below is an example implementation of the Unwrap
method:
func(e*PathError)Unwrap()error{
return e.Err
}
errors.Is
: With this function, you can compare an error value against the sentinel value. What makes this function different from our usual error checks is that instead of comparing the sentinel value to one error, it compares it to every error in the error chain. It also implements an Is
method on an error so that an error can post itself as a sentinel even though it’s not a sentinel value.
func Is(err, target error) bool
In the basic implementation above, Is
checks and reports if err
or any of the errors
in its chain are equal to target (sentinel value).
errors.As
: This function provides a way to cast to a specific error type. It looks for the first error in the error chain that matches the sentinel value and if found, sets the sentinel value to that error value and returns true
:
package main
import (
"errors"
"fmt"
"os"
)
func main() {
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)
}
}
}
You can find this code in Go’s source code.
Compiler result:
Failed at path: non-existing
Program exited.
An error matches the sentinel value if the error’s concrete value is assignable to the value pointed to by the sentinel value. As
will panic if the sentinel value is not a non-nil pointer to either a type that implements error or to any interface type. As
returns false if err
is nil
.
Summary
The Go community has been making impressive strides as of late with support for various programming concepts and introducing even more concise and easy ways to handle errors. Have you got any ideas on how to handle or work with errors that may appear in your Go program? Do let me know in the comments below.
Resources:
Go’s programming language specification on Type assertion
Marcel van Lohuizen’s talk at dotGo 2019 – Go 2 error values today
Go 1.13 release notes
Editor's note: Seeing something wrong with this post? You can find the correct version here.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post Error handling in Golang appeared first on LogRocket Blog.
Top comments (2)
nice information :)
great resource