Editor’s note: This article was last updated and validated for accuracy on 18 November 2022.
Tackling errors in Go requires a different approach than the conventional methods in other mainstream programming languages, like JavaScript, which uses the try...catch
statement, or Python with its try… except
block. Oftentimes, developers misapply Go’s features for error handling.
In this article, we’ll consider the best practices that you should follow when handling errors in a Go application. To follow along with this article, you’ll need a basic understanding of how Go works. Let’s get started!
The blank identifier is an anonymous placeholder. You can use it just 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 surrounding unused imports and variables in a program.
Assigning errors to the blank identifier instead of properly handling them is unsafe, meaning 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. }
The rationale behind this is likely that you’re not expecting an error from the function. However, this could create cascading effects in your program. The best practice is to handle errors whenever you can.
One way to handle errors is to take advantage of the fact that functions in Go support multiple return values. Therefore, 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 snippet above, we have to return the predefined error
variable if we think there’s a chance that 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 that there is 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 }
Whenever the iterate
function is called and err
is not equal to nil
, the error returned should be handled appropriately.
One option could be to create an instance of a retry or a cleanup mechanism. The only drawback with handling errors this way is that there’s no enforcement from Go’s compiler. You have to decide how the function you created returns the error.
You could 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 the code below in 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, making it very difficult to locate where an error occurred. The error could pass through a number of functions before being printed out.
To handle this, you could install the pkg/errors
package, which provides basic error handling primitives like stack trace recording, error wrapping, unwrapping, and formatting. To install this package, run the command below in your terminal:
go get github.com/pkg/errors
When you need to add stack traces or any other information to make debugging your errors easier, 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
, and %+v
:
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. 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
Although Go doesn’t have exceptions, it has a similar type of mechanism known as defer, panic, and recover. Go’s ideology is that adding exceptions like the try/catch/finally
statement in JavaScript would result in complex code and encourage programmers to label too many basic errors, like failing to open a file, as exceptional.
You should not use defer/panic/recover
as you would throw/catch/finally
. You should reserve it 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() }
The code above would compile as follows:
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. Therefore, you should use panic
only in rare cases where it’s not safe for your code or anyone integrating your code to continue at that point.
The code sample below demonstrates 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 follows:
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 built-in recover
function to handle panic
and return the values passed from a panic call. recover
must always be called in a defer
function, otherwise, 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 you can see in the code sample above, recover
prevents the entire execution flow from coming to a halt. We added in a panic
function, so the compiler would return the following:
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 where 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. Therefore, you can set a returned variable before a return statement is executed. The code sample above would compile as follows:
If it's more than 100 degrees... Then there's nothing we can do Program exited.
Previously, error wrapping in Go was only accessible via packages like pkg/errors
. However, Go v1.13 introduced support for error wrapping.
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
has a %w
verb, and for inspecting and unwrapping errors, a couple of functions have been added to the error
package.
errors.Unwrap
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 }
With the errors.Is
function, you can compare an error value against the sentinel value.
Instead of comparing the sentinel value to one error, this function 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 the target, the sentinel value.
The errors.As
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, it sets the sentinel value to that error value, returning 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. Below is the 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
.
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. Do you have any ideas on how to handle or work with errors that may appear in your Go program? Let me know in the comments below.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]