9. Errors

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.

Errors as Values

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

The error Interface

In Go, errors are represented by the error2 interface, as shown in Listing 9.3.

2. https://pkg.go.dev/builtin#error

Listing 9.3 The error Interface

Creating Errors with errors.New

Go provides two quick ways to implement the error interface in your code.

The first is through the errors.New3 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

Creating Errors with 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.Errorf4 method, Listing 9.6, can be used.

4. https://pkg.go.dev/fmt#Errorf

Listing 9.5 Creating Errors with errors.New and fmt.Sprintf5

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.

Handling 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

Using Errors

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

Panic

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

Raising a Panic

A panic in Go can be raised using the built-in panic6 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

Recovering from a Panic

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 recover7 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.

Capturing and Returning Panic Values

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:

  1. Use a defer with a recover to catch the panic.

  2. Use type assertion on the value returned from the panic to see if it was an error.

  3. 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

Don’t Panic

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.

Checking for Nil

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

Maps

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 make9 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

Pointers

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

Interfaces

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.Write11 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.Stdout12 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

Functions

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

Type Assertions

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

Array/Slice Indexes

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 error13 instead of panicking.

13. https://pkg.go.dev/builtin#error

Listing 9.43 Proper Type Assertion Checking Prevents Panics

Custom Errors

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

Standard Errors

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.

Defining Custom Errors

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

Wrapping and Unwrapping 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

Wrapping Errors

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

Unwrapping Errors

You can use the errors.Unwrap14 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

Unwrapping Custom Errors

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

To Wrap or Not To Wrap

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.

Errors As/Is

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.Is15 and errors.As16—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

As

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

Is

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

Stack Traces

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/debug17 package provides a couple of functions that you can use to get, or print, a stack trace. The debug.Stack18 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.PrintStack19 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

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset