Adding context to an error / custom errors

So far, we have seen the simple types of an error where an error is a simple struct with a string field which represents the error message. But in the real world, we need to add more context to the error.

So what could be the context? If you have multiple nested function calls and in runtime, you encounter an error. We need to know where that error occurred. By adding some information to your error, we can easily debug the error. A context is a piece of information about the environment where the error occurred which is available on the error itself.

Let’s say, for an example, an HTTP call returns an error (for non-200 status). So while handling the error, we also need to know what the HTTP status was and method of the request. Let’s create a mock function which sends an HTTP request and returns the response of the call.

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, hence errVal := 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 were sure that err interface holds the value of type *HttpError but in the cases when you are not sure, you can use the second version of type assertion which is errVal, ok := err.(*HttpError) where ok will be false when err does not hold the value of type *HttpError . Hence, you also need to check in the if condition if ok is true.

Similarly, using type switch, we can conditionally check the type of an error and act against it specifically.

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.

Saving original error as a context (wrapping an error)

You would be thinking, this all seems pretty tedious to do in practice. Well, we can simplify the process of adding a context to an error by using some of the properties of a struct and methods, like a method promotion.

If you just want to add some context to an existing error, we can create a struct which contains some context and the original error. Also, we can add additional information to the error message returned by the Error method.

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 .

Adding context to error message is meant only for the debugging purpose. If you want to utilize context information, you can use type assertion to extract underlying error object.

You can modify the above example by using anonymous nested error. As we have seen in promoted fields and promoted methods lessons, we can set error as a field name and type on UnauthorizedError struct type.

From the above example, we have removed the Error method on UnauthorizedError because Error method on type error will be promoted to UnauthorizedError and we still have some context on the error err . We need to use the type assertion to extract the error to get the UserId .

Getting error context using methods

Since an error is a struct and struct can have methods, our error struct can have methods which provided additional information about the error.

In the above example, we have created a new method IsUserLoggedIn which returns true or false based on SessionId field of the UnauthorizedError . Inside main function, we are extracting the dynamic value of err interface which is errVal and it has the IsUserLoggedIn method which gives us extra information about the user’s logged in state.

Custom error interfaces

In the embedded interfaces lesson, we have learned that an embedded interface can be created by merging multiple interfaces. By using this principle, we can have an interface which contains the error interface and some extra method. This interface will contain Error method from error interfaces since it was promoted. A struct which implements this interface will be an error because the only necessary condition is a type should implement the Error method to qualify as an error.

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.

Adding stack-trace to an error

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.