This chapter covers the benefits of how Go’s error model results in more reliable code. We cover how to handle basic errors and return errors as an interface that satisfies the error type. Additionally, we also discuss concepts such as custom error types, panics, recovering from panics, and sentinel errors.
A lot of languages use the concept of exceptions.1 When something goes wrong, an exception is thrown. This exception then needs to be caught. When catching an exception, you have the opportunity to log the exception and possibly move on with an alternate code path or reraise the exception to let the developer upstream deal with the problem.
1. https://en.wikipedia.org/wiki/Exception_handling
Listing 9.1 shows an example of how to handle exceptions in Java.
Listing 9.1 Exceptions in Java
Go takes a different approach and treats errors as values. These values are returned and managed instead of throwing and capturing exceptions, Listing 9.2.
Listing 9.2 Errors in Go
error
InterfaceIn Go, errors are represented by the error
2 interface, as shown in Listing 9.3.
2. https://pkg.go.dev/builtin#error
Listing 9.3 The error
Interface
errors.New
Go provides two quick ways to implement the error
interface in your code.
The first is through the errors.New
3 function, Listing 9.4. This function takes a string and returns an implementation of error
that uses the supplied string as the error message.
3. https://pkg.go.dev/errors#New
Listing 9.4 The errors.New
Function
fmt.Errorf
(Recommended)When creating errors, it is common to want to create a string that contains the error message and the values of variables that caused the error.
To clean up the pattern shown in Listing 9.5, the fmt.Errorf
4 method, Listing 9.6, can be used.
4. https://pkg.go.dev/fmt#Errorf
Listing 9.5 Creating Errors with errors.New
and fmt.Sprintf
5
5. https://pkg.go.dev/fmt#Sprintf
Listing 9.6 The fmt.Errorf
Method
In Listing 9.7, the code is now cleaner and more readable than in Listing 9.5.
Listing 9.7 Creating Errors with fmt.Errorf
Using fmt.Errorf
handles most use cases for creating errors.
With errors being a type in the Go type system, in this case an interface
type, errors can be returned from, and accepted as, function arguments.
In Go, if an error is returned from the function, it should always be returned as the last argument. While the Go compiler won’t enforce this, it is the expected idiom in the Go ecosystem. In Listing 9.8, the functions one
and two
idiomatically return an error value as the last argument. The bad
function, however, is nonidiomatic by returning an error value as its first argument.
Listing 9.8 Good and Bad Examples of Returning an error
As with all interfaces in Go, the zero value of the error
interface is nil
. Error checking in Go is done by checking whether the returned error is nil
, as shown in Listing 9.9.
Listing 9.9 Checking for Errors by Asserting Against nil
In Listing 9.10, if the key being requested in the map is found, its value is returned, and a nil
is returned instead of an argument. If the key is not found in the map, an empty string and an error, created with fmt.Errorf
, are returned.
Listing 9.10 A Function That Might Return an error
A test for the Get
function, Listing 9.11, demonstrates the error-checking pattern in action.
Listing 9.11 A Test for the Get
Function
Occasionally, your code does something that the Go runtime does not like. For example, in Listing 9.12, if we try to insert a value into an array or slice that is beyond the bounds of the array or slice, the runtime panics.
Listing 9.12 A Panic Caused by an Out-of-Bounds Index
A panic in Go can be raised using the built-in panic
6 function, Listing 9.13. The panic
function takes any
value as an argument.
6. https://pkg.go.dev/builtin#panic
Listing 9.13 The panic
Function
With a combination of the defer
keyword and the recover
function, shown in Listing 9.14, you can recover from panics in your applications and gracefully handle them.
Listing 9.14 The recover
7 Function
7. https://pkg.go.dev/builtin#recover
In Listing 9.15, before we run the code that will panic, we use the defer
keyword to execute an anonymous function that runs before the main
function exits. Inside of the deferred function, we can call the recover
function and check its return value for nil
. A non-nil
value is returned from the recover
function if a panic occurred.
Now, when the panic occurs, it is caught by the deferred recover
and can be handled gracefully.
Listing 9.15 Recovering from a Panic
While this is not the common use case for using recover
, it does show the mechanics of how it works. It is more common to use recover when your application calls a user-defined function that is passed in as an argument.
Listing 9.16 is an example of a function that takes a user-defined function to match a specific string. If the function passed in panics, it results in the sanitize
function panicking as well.
Listing 9.16 A Function That Sanitizes a Given String
However, if we use a recover
in the sanitize
function, we can gracefully handle any potential panic that the user-provided function may create. In Listing 9.17, we use the recover
to handle panics in the sanitize
function.
Listing 9.17 A Function That Sanitizes a Given String
Now, if a user inadvertently raises a panic in the matcher
function provided, the sanitize
function handles it gracefully and returns an error instead of panicking as well.
When something panics in Go, you have three options for how to handle the panic:
You can let the panic crash the application and deal with the fallout.
You can recover from the panic, log it, and move on.
You can properly capture the panicked value and return it as an error.
This last option gives you the most control over recovering from panics. However, it requires a number of steps, and functions, to make this happen.
Consider the function, DoSomething(int)
, in Listing 9.18 that takes an integer and either returns nil
or panics.
Listing 9.18 A Function That Returns nil
or Panics
In Listing 9.19, we have a test for the DoSomething(int)
function. When we call the DoSomething(int)
function with the value 1
, the test panics.
Listing 9.19 A Test for the DoSomething(1)
Function in Listing 9.18
To fix this problem, we need to properly recover from the panic being raised in the DoSomething(int)
function. The following list outlines the steps required to properly recover from a panic:
Use a defer
with a recover
to catch the panic.
Use type assertion on the value returned from the panic to see if it was an error
.
Use a named
return to allow sending back the error from the deferred recover.
In Listing 9.20, we implement the steps we outlined to properly recover from the panic. As shown in Listing 9.20, we implemented the fix in the DoSomething(int)
function and not in the test. This is because it is the responsibility of the function that panics to properly recover from it.
Listing 9.20 Properly Returning from a Panic
First, we have changed our function signature to use a named return for the error
,8 (err error)
. This enables us to set the error
value inside the deferred function. Once inside the DoSomething(int)
function, we use the defer
keyword and an anonymous function to catch the panic. Inside of the anonymous function, we use the recover
function to recover from the panic, and assign the value returned to the variable p
.
8. https://pkg.go.dev/builtin#error
As shown earlier in this chapter in Listing 9.14, the recover
function returns any
. This means that the value can be of any type, a string
, int
, an error
, or nil
. As a result, we must use type assertions to check the type of the value returned from the recover
function. If the value is an error
, we can use the named
return to send the value back to the caller. If not, we create a new error
with the value returned from the recover
function.
As shown in Listing 9.21, the tests now are no longer panicking when run, and the test passes.
Listing 9.21 Tests Now Pass After Proper Panic Recovery
NEVER RAISE A PANIC IN YOUR CODE
Ok, maybe never is too strong of a stance. However, in general, it’s considered nonidiomatic to panic instead of returning an error outside of specific conditions.
When a panic occurs, unless there is a recover in place, the program shuts down (usually not gracefully). It is usually a much better practice to return an error and let the code upstream handle the error.
The general rule is that if you are writing a package, you should not panic. The reason for this is that the caller should always have control of the program, and a package should not dictate control flow of a program.
On the other hand, if you are the caller (maybe you are in control of the main
function in the program), then you already have total control of the program flow, and if needed, a panic may be appropriate. Many times this is manifested in the form of a log.Fatal
, Listing 9.22, which exits the program with a nonzero exit code.
Listing 9.22 The log.Fatal
Function
Lastly, any code that panics may be more difficult to test. For all of these reasons, it is best to consider alternatives to a panicking. After all, most panics can be prevented by sensible code and well-designed tests.
The most common source of panics in Go are calls being made on nil
values. Any type in Go that has a zero value of nil
can be a source of these panics. Types such as interfaces, maps, pointers, channels, and functions all fall into this category. Checking whether these types are nil
before using them can help you avoid these panics.
A common example of this is when a type has an embedded type that is a pointer. In Listing 9.23, User
is embedded in Admin
as a pointer. Because it’s embedded, the methods are promoted. This means that the Admin
type now has a String
method. However, on the last line of code where a.String()
is being called, the receiver to the method is actually the User
. Because User
is nil
, it was never set, and the code panics when it tries to access the String
method.
Listing 9.23 A Panic When Calling a Method on a nil
Value
In Listing 9.24, the Admin
type is properly initialized with a User
. Now, when the String
method is called, the User
is not nil
, and the method properly executes.
Listing 9.24 Proper Initialization to Prevent Panicking
When creating maps, you must initialize
the memory space behind them. Consider Listing 9.25 as an example. We have created a map variable, but we have not initialized it. This causes a panic when we try to access the map.
Listing 9.25 A Panic When Creating a Map with a nil
Value
The easiest solution is to use the :=
operator to initialize the map when the variable is declared. In Listing 9.26, we have initialized the map, and the code runs successfully.
Listing 9.26 Avoiding Panics When Creating Maps
However, if a map is declared with long variable declaration and not initialized, later in the code, the map needs to be initialized or it panics when used. As shown in Listing 9.27, we check the m
variable against nil
before using it. If the variable has not been initialized, we can initialize a new map and assign it to the variable. This prevents panics from trying to access a nil
map.
Listing 9.27 Checking a Map for nil
before Accessing
Finally, as in Listing 9.28, we can use the make
9 function to initialize the map. While this works, it is considered nonidiomatic to use make
to initialize a map.
9. https://pkg.go.dev/builtin#make
Listing 9.28 Using make
to Initialize a Map
Before a pointer can be used, it has to be initialized. In Listing 9.29, we define a new variable, bb
, that is a pointer to a bytes.Buffer
. If we try to use this variable before the pointer has been properly initialized, a runtime panic occurs.
In Listing 9.30, we declare and initialize the bb
variable with a pointer to a bytes.Buffer
. Because the bb
variable has been properly initialized, the application no longer panics.
Listing 9.29 A Panic Caused by a nil
Pointer
Listing 9.30 Properly Initializing a Pointer before Use
In Listing 9.31, we create a new variable, w
, of type io.Writer
. This variable has not been initialized with an implementation of the interface. This causes a panic when we try to use the variable.
Listing 9.31 A Panic Caused by a nil
Interface
In Listing 9.32, we have initialized the w
variable with an implementation of the interface, in this case io.Stdout
.10
10. https://pkg.go.dev/io#Stdout
Listing 9.32 Initializing an Interface before Use
More commonly, a panic occurs when an interface is embedded into a type and that interface was not backed by an instance.
In Listing 9.33, we are defining a Stream
struct that embeds the io.Writer
interface. By embedding the interface, the Stream
type is now also an implementation io.Writer
as the io.Writer.Write
11 method is promoted to the Stream
type.
11. https://pkg.go.dev/io#Writer.Write
Listing 9.33 A Custom Type That Embeds an Interface
However, if an instance of a writer is never assigned to the embedded io.Writer
, as in Listing 9.34, the code panics when it tries to call the io.Writer.Write
method because the receiver is nil
.
Listing 9.34 A Panic Caused by a nil
Interface
In Listing 9.35, we fix the code by properly assigning an implementation of an io.Writer
interface—in this case, os.Stdout
12 to the embedded io.Writer
in the Stream
type.
12. https://pkg.go.dev/os#Stdout
Listing 9.35 Properly Assigning an Implementation of an Embedded io.Writer
A function’s zero value is nil
. As such, a function needs to be assigned before using it. In Listing 9.36, we have created a function variable, fn
, that is not backed by an actual function. This causes a panic when we try to call the function.
Listing 9.36 Using an Uninitialized Function Variable
In Listing 9.37, we have assigned a function to the fn
variable and the application runs successfully.
Listing 9.37 Assigning a Function Definition to the fn
Variable
When type asserting in Go, if the assertion fails, Go panics.
Consider the WriteToFile(io.Writer, []byte)
function defined in Listing 9.38. This function takes an io.Writer
as an argument, along with a []byte
slice of data to write. Inside of the function, the io.Writer
argument, w
, is being type asserted to the concrete type os.File
. In Listing 9.39, we are calling this function with a bytes.Buffer
as the first argument, which is not an instance of os.File
. This causes a panic.
Listing 9.38 Type Assertion without Safety Checking
Listing 9.39 Panic Caused by a Failed Type Assertion
If asked, Go returns a second argument, a boolean, during the assertion. That boolean is true
if the assertion was successful and false
if it wasn’t. Checking this second argument, as shown in Listing 9.40, prevents panics on type assertion failures and keeps your application up and running.
Listing 9.40 Proper Type Assertion Checking Prevents Panics
When accessing indexes on slices and arrays if the index is greater than the length of the slice/array, Go panics.
Consider the function defined in Listing 9.41. This function attempts to access the given index of the given slice. If the index is greater than the length of the slice, Go panics, Listing 9.42.
Listing 9.41 A Function for Retrieving the Index of a Slice
Listing 9.42 An Out-of-Bounds Panic
To avoid this panic, check the length of slice/array to ensure the requested index can safely be retrieved. In Listing 9.43, the index is checked against the length of the slice/array before being accessed. If the index is greater than the length of the slice/array, the function returns an error
13 instead of panicking.
13. https://pkg.go.dev/builtin#error
Listing 9.43 Proper Type Assertion Checking Prevents Panics
Errors in Go are implemented via the error
interface, Listing 9.44. This means you can create your own custom error implementations. Custom errors allow you to manage workflow and provide detailed information about an error beyond the scope of the error
interface.
Listing 9.44 The error
Interface
Consider Listing 9.45. A couple of types are being defined. The first, Model
, is based on map[string]any
. For example, {"age": 27, "name": "jimi"}
. Next, a Store
struct type is defined with a data
field of type map[string][]Model
. This data
field is a map of table names to their models.
Listing 9.45 Type Definitions
The Store
has a method, All(string)
, shown in Listing 9.46, that returns all of the models in the store for the given table. If the table doesn’t exist, an error is returned. This error is created using the fmt.Errorf
function.
Listing 9.46 The Store.All
Method
Listing 9.47 is a test that asserts whether a given table exists in the store. This test, however, is lacking. Yes, it asserts that an error was returned, but we don’t know which error was returned. Did the All
method return an error because the table didn’t exist or because the underlying data
map was nil
?
Listing 9.47 A Test for the Store.All
Method
As in Listing 9.47, it might be tempting to assert against an error’s message, but that is considered nonidiomatic and should never be used. The reason for this is simple: Error messages change. If the error message changes, the following test fails. We consider this to be a “brittle” test.
You can define custom errors that can help distinguish one error from another, as well as add more context about the error.
In Listing 9.48, we define a new struct
type, ErrTableNotFound
, that implements the error
interface. This type contains information such as what table was missing and what time the error occurred.
Listing 9.48 The ErrTableNotFound
Struct
Listing 9.49 updates the Store.All(string)
method to return an ErrTableNotFound
error if the table doesn’t exist. We also set the table name and the time the error occurred.
Listing 9.49 The Store.All
Method with Custom Errors
The test can now be updated, Listing 9.50, to assert that the error is an ErrTableNotFound
error. The test passes, and we can see the table name and the time the error occurred.
Listing 9.50 A Test for the Store.All
Method with Custom Errors
Consider the method defined in Listing 9.51. If an error occurs, a custom error
implementation, ErrTableNotFound
, is initialized with the appropriate information. Before being returned, however, error
is then wrapped with fmt.Errorf
to a message that includes the type and method that caused the error. To wrap an error using fmt.Errorf
, we can use the %w
formatting verb, meant for errors.
Listing 9.51 The Store.All
Method
Listing 9.52 shows a test for the method in Listing 9.51. This test tries to assert the returned error is of type ErrTableNotFound
. When an error is wrapped with fmt.Errorf
, the resulting type is that of the general error
interface. As such, by wrapping the ErrTableNotFound
error with fmt.Errorf
, we have changed the resulting type of the error. This also results in the tests failing as the error is no longer an ErrTableNotFound
, but a different type.
Listing 9.52 A Wrapped Error Can No Longer Be Asserted against Correctly
To get to the original error, we need to unwrap the errors until we reach the original error. This is similar to peeling an onion, where each error is a layer of wrapping.
Let’s take a simplified look at how wrapping and unwrapping errors works in Go. Consider the error
types defined in Listing 9.53. Each one contains an err
field that holds the error that is wrapping.
Listing 9.53 The Three Error Types
In Listing 9.54, the Wrapper
function takes an error and the proceeds to wrap it in each of the three error types.
Listing 9.54 The Wrapper
Function
Another way to do this same wrapping is to use multiline initialization and fill each error type with the next error type, as shown in Listing 9.55. Both of these are valid implementations of the same wrapping.
Listing 9.55 The Wrapper
Function
You can use the errors.Unwrap
14 function, Listing 9.56, to unwrap an error until it reaches the original error. This will continue to peel the wrapped layers until it, hopefully, reaches the original error.
14. https://pkg.go.dev/errors#Unwrap
Listing 9.56 The errors.Unwrap
Function
In Listing 9.57, the test has been updated to use the errors.Unwrap
function to unwrap the error until it reaches the original error. Unfortunately, however, the test fails because the result of errors.Unwrap
is nil
.
Listing 9.57 Using errors.Unwrap
to Get the Original Error
As the documentation states (refer to Listing 9.56), errors.Unwrap
returns the result of calling the Unwrap
method on an error if the error’s type contains an Unwrap
method returning error
. Otherwise, errors.Unwrap
returns nil
.
What the documentation is trying to say is that your custom error types need to implement the interface shown in Listing 9.58. Unfortunately, the Go standard library does not define this interface for you outside of the documentation.
Listing 9.58 The Missing Unwrapper
Interface
Let’s update the error types defined in Listing 9.53 to implement the Unwrapper
interface. Although Listing 9.59 only shows one implementation, all of our types need to be made to implement the Unwrapper
interface. The implementation of this interface needs to make sure to call errors.Unwrap
on the error it is wrapping.
Listing 9.59 Implementing the Unwrapper
Interface
The tests in Listing 9.59, however, are still failing. If the error we are wrapping does not contain an Unwrap
method, the errors.Unwrap
function returns nil
, and we can’t get access to the original error.
To fix this, we need to check the error we are wrapping for the Unwrapper
interface, as shown Listing 9.60. If it does, we can call errors.Unwrap
with the error. If it does not exist, we can return the error as is.
Listing 9.60 Properly Implementing the Unwrap
Method
With an understanding of errors.Unwrap
, we can fix the test in Listing 9.61 to get the original error.
Listing 9.61 The WrappedError
Type
You’ve seen that you can use the fmt.Errorf
with the %w
verb to wrap an error. This allows for the unwrapping
of an error later, either from the caller of your function/method or in a test.
While the normal rule is always to wrap the errors with the %w
, there are exceptions. If you don’t want some internal information or package-specific information not to be wrapped, then it is acceptable to use the %s
verb to hide any implementation details. Keep in mind that this is normally the exception and not the rule.
If in doubt, it is usually safer to wrap the error so that other code that calls your package can check for specific errors later.
While unwrapping an error allows us to get to the original, underlying error, it does not allow us to get access to any of the other errors that it might have been wrapped with. Consider Listing 9.62. When we unwrap the error returned from the Wrapper
function, we can get access to the original error passed in, but how do we check whether the wrapped error has ErrorB
in its stack and how do we get access to the ErrorA
error? The errors
package provides two functions—errors.Is
15 and errors.As
16—that will help us with these questions.
15. https://pkg.go.dev/errors#Is
16. https://pkg.go.dev/errors#As
Listing 9.62 A Function That Nests One Error in Many Errors
When working with errors, we often don’t care about the underlying error. There are times, however, when we do care about the underlying error, and we want to get access to it. The errors.As
, Listing 9.63, function is designed to do this. It takes an error
and a type to match against. If the error matches the type, it returns the underlying error. If the error does not match the type, it returns nil
.
Listing 9.63 The errors.As
Function
Like the errors.Unwrap
function, errors.As
also has a documented, but unpublished, interface, Listing 9.64, that can be implemented on custom errors.
Listing 9.64 The AsError
Interface
For the errors.As
function to work properly, we need to implement an As
method on our error types. In Listing 9.65, you can see an implementation of this function for the ErrorA
type. This method is called when errors.As
is called on our error. The As
method should return true
if the error matches the target and false
otherwise. If false
, we need to call errors.As
on our error’s underlying error. If true
, we can return true
and set the target to the current error.
Listing 9.65 Implementing the AsError
Interface
It is important to note that to set the target to the current error, we must first dereference the target pointer. This is because the As
method is responsible for setting the target to the current error. If we don’t dereference the target pointer, any changes we make would be lost when the As
method returns.
As we can see from the test in Listing 9.66, we are able to take the wrapped error and extract the ErrorA
type from the error stack. The As
method sets the value of act
to the error in the stack, and we are able to then access the ErrorA
type directly.
Listing 9.66 Testing the AsError
Implementation
While the errors.As
function, Listing 9.63, is used to check for the type of an error, the errors.Is
function, Listing 9.67, is used to check if an error in the error chain matches a specific error type. This provides a quick true/false
check for an error type.
Listing 9.67 The errors.Is
Function
The errors.Is
documentation, like errors.As
and errors.Unwrap
, has a documented but unpublished interface. This interface is defined in Listing 9.68.
Listing 9.68 The IsError
Interface
Like with errors.As
, we have to implement the Is
method for our custom error types, Listing 9.69. If our error type is the same type as the target
error, then we can return true
. If the target
error is not a match, we then need to call errors.Is
with our underlying error and the target
error so that error can be checked as well.
Listing 9.69 Implementing the IsError
Interface
Finally, in Listing 9.70, we can write a test to assert that our Is
method works as expected.
Listing 9.70 Testing the IsError
Implementation
Using a stack trace to debug your code can be very helpful at times. A stack trace shows where you are in the code and how you got there by printing a list of all calling functions.
The runtime/debug
17 package provides a couple of functions that you can use to get, or print, a stack trace. The debug.Stack
18 function, Listing 9.71, returns a slice of bytes that represent the stack trace.
17. https://pkg.go.dev/runtime/debug
18. https://pkg.go.dev/runtime/debug#Stack
Listing 9.71 The debug.Stack
Function
The debug.PrintStack
19 function, Listing 9.72, prints the stack trace to the standard output.
19. https://pkg.go.dev/runtime/debug#PrintStack
Listing 9.72 The debug.PrintStack
Function
In Listing 9.73, we print the stack trace of a program to standard output using debug.PrintStack
.
Listing 9.73 Printing a Stack Trace
In this chapter, we discussed Go’s error handling in depth. We covered error handling, and creation, in our code. We showed you how to create custom implementations of error
interface. Next, we demonstrated how a panic can crash an application, and we discussed various ways to recover from panics. We showed you that you can use errors.Unwrap
to try to get the original error from a wrapped error. We also explained how to use errors.As
to try to assert an error has a certain type in its chain, and if so, it binds the error to a variable to be used in the rest of the function. Finally, we discussed how to use errors.Is
to check whether an error in the chain is of a certain type.