Error handling in Go

Go does not provide conventional try/catch method to handle the errors, instead. In this article, we are going to explore error handling in Go.

Conventionally, we learned an error is something that is fatal to the program but in Go, error has a different meaning. An error is just a value that a function can return if something unexpected happened.

So what could happen in a function that is unexpected? For example, the function was invoked with wrong arguments or execution inside the function did not go as expected. In that case, this function can return an error as a value.

error is a built-in type in Go and its zero value is nil. An idiomatic way to handle an error is to return it as the last return value of a function call and check for the nil condition.
val, err := myFunction( args... );
if err != nil { // handle error } else { // success }

If you are familiar with the Node.js, then any async function callback returns an error as the first argument. In Go, we follow the same norm.

Let’s first understand what error looks like. As we know the error is a built-in type but in reality, it is an interface type made available globally and it implements Error method which returns a string error message.
type error interface { Error() string }

As we learned from interfaces lesson, the zero value of an interface is nil. Hence, any type that implements the error interface is as an error. So, let’s create our first error by implementing error interface.

From the above example, we have created a struct type MyError which implements Error method. This method returns a string. Hence, struct MyError implements the error interface. So in nutshell, myErr is an error now. Println function is designed to call Error method automatically when the value is an error hence error message something unexpected happened message got printed.

You are free to call Error method on an error if you want to inspect its output. You can also have Error method with value receiver instead of pointer received as used in the above example. You can read more about in the structs lesson.

But to create even a simple error, we need to define a struct type and make it implement the Error method. To avoid this, Go provides the built-in errors package which exports the New function. This function expects an error message and returns an error.

You can also use Errorf function from fmt package to create interpolated error messages. Here is an example from official Go documentation.

So from the above example, we can see that New function expects a string which is an error message and returns an error. Since we know an error is anything that implements the error interface, what is the type myErr then?

From the above example, we can see that the type of myErr is *errors.errorString which is a pointer to errors.errorString. When we see the value using %#v formatting verb, myErr is a pointer to a struct which has s string field.

Now, we can speculate that New function must return a pointer to the errorString struct invoked with the string passed to it. This is exactly what’s happening behind the scene. The errors package has errorString struct which implements the error interface.
type errorString struct { s string } func (e *errorString) Error() string { return e.s }

The New function creates and returns the pointer to the errorString struct.
func New(text string) error { return &errorString{text} }

Now, it’s time to implement error handling. As we know, an error is just like a value returned by the function (as the last value), and to create an error we can use New function from the errors package, everything seems simple.

In the above example, we have created the Divide function which takes two integers and returns the division. It also checks if the divisor is zero. In case when the divisor is zero, the function returns a non-nil error.
If you come from the Node.js background, then you are used to having an error as the first parameter of a callback function of an async call. I personally don’t like Go’s ways to return an error as the last return value, I wish if would be the first.
. . .

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

Never miss a post from Aditya Agrawal, when you sign up for Ednsquare.