Let me walk through the above example one point at a time. First, we created the HttpError struct which has status and method fields. By implementing Error method, it implements the error interface. Here, Error method returns a detailed error message by using status and method values.
GetUserEmail is a mock function which is designed to send an HTTP request and return the response. But for our example, it returns empty response and an error. Here error is a pointer to the HttpError struct with 403 status and GET method fields.
Inside the main function, we call GetUserEmail function which returns the response string (email) and an error (err). Here, the type of err is error which is an interface. From the interfaces lesson, we learned that to retrieve the underlying dynamic value of an interface, we need to use type assertion.
Since the dynamic value of the err has *HttpError type, henceerrVal := err.(*HttpError) returns a pointer to the instance of the HttpError. Hence, errVal contains all the contextual information and we extract this information and do some conditional operations.
Let’s understand the meaning behind this facade. An error is something that implements the error interface. An interface can represent many different types. Hence any error can have many different types. A type of error returned by errors.New is of *errors.errorString type. Similarly, in the above example, it is of *HttpError type.
Using type assertion syntax which is err.(TypeOfError), we can extract the context of the error which is nothing but the dynamic value of the error err.
In the above example, we have created two error types viz. NetworkError and FileSaveFailedError. The saveFileToRemote function can return any of these errors or nil in the case when error did not occur.
In the main function, using a type switch statement, we extracted the dynamic type and matched against various cases to do conditional operations.
In the above example, we have created a UnauthorizedError struct type, which contains UserId and OriginalError fields. The OriginalError field stores an error. Inside Error method, we are adding context to the original error. %v formatting verb will call Error method on httpErr.OriginalError and inject returned string.
The validateUser returns an instance of UnauthorizedError which contains the original error err which was created using fmt.Errorf function.
Inside the main function, we can call validateUser function and read the error. fmt.Println will call Error method on the err struct which returns an original error message with the context added from err.OriginalError.
Don’t get confused by the above example, it’s very simple to understand. We have an interface type UserSessionState which contains isLoggedIn method and getSessionId method. It also has an embedded interface error which promotes Error method. Hence, UserSessionState can be used as a type that represents an error.
Since UnauthorizedError struct implements both getSessionId and isLoggedIn methods as well as Error method, it implements UserSessionState interface.
In the main function, err has the static type of error but the dynamic type of UnauthorizedError. Since we learned in the interfaces lesson that using type assertion we can convert the dynamic value of an interface to an interface type that it implements.
In line no 51, we are doing exactly the same. Since the dynamic value of err is the type of *UnauthorizedError but since UnauthorizedError implements UserSessionState interface, it returns the static type UserSessionState which has a dynamic value of *UnauthorizedError instance returned from validateUser function.
This way, we can call getSessionId() method on errVal which is an interface of type UserSessionState. Since it has the dynamic value of *UnauthorizedError, we need to use type assertion again to extract it. This has shown in the comment on line no 56.
So far, we have learned how to create an error and how we can add context information to it. If you are coming from other programming languages background, then you would be worried, where is the stack trace?
Stack trace gives us exact information about where the error occurred (or returned) in our code. When an error occurs, a stack trace is a great way to debug your code as it contains the filename and the line number at which the error has occurred and a stack of function calls made until the error occurred.
Unfortunately, Go does not provide the capability to a stack trace to an error.
Here, we need to depend on a Go package published here on GitHub. This package provides Wrap method which adds context to the original error message as well as Cause method which used to extract original error.
You can follow the documentation on how to import this package from their official documentation. This package also provides New and Errorf functions so we don’t need to use built-in errors package.
In the above program, we have created a simple error originalError using New function and provided an error message (line no. 9). To add context information to this error, we used Wrap function from error package (line no. 11). This adds extra information to the original error message as well as adds a stack trace.
If you don’t need to see the stack trace, you can simply print the error using Println or Printf function using %v formatting verb (line no. 15). To extract the stack trace as well as the original error message, we use %+v formatting verb (line no. 18).
To extract the original error, you can use Cause function (line no. 21). Any error which implements causer interface (which contains Cause() error method) can be inspected by the Cause function.
One great feature of Wrap function is if the error passed to this function is nil, then return value will be nil. This is useful in case when you want to wrap and return an existing error, otherwise, we would have to check the error for nil condition in order to add context information manually.
In my opinion, you should only add stack trace to an error which is potentially going to break your program. A logical error like we have seen in the authorization example does not need a track state. But since Wrap method can be used to ammend original error message which is also called as annotating an error, the choice is up to you.
In nutshell, we understood that Go treats an error as a value and want developers to handle it gracefully. Using structs and interfaces, we can create custom errors and using type assertion or type switch we can handle them conditionally. This is a great plan but there is a drawback.
When you ship a package or a module for other people to use, you need to export all the error types so that your consumers can handle them conditionally. If you are making error types a part of your public API, then you have another thing to maintain and worry about. The solution is to avoid error types when you can, if possible.