Introduction
Go is a very powerful language with a not-so-steep learning curve. The reason why I love Go is because of its versatile nature. The versatile nature of go can be seen in how the authors envisioned errors and error handling.
In Go, an error is a builtin type which makes it a first-class citizen. The reason why it is very powerful is because of its simplicity. It is defined as a simple interface like below
type error interface {
Error() string
}
This gives you the freedom to return any data you want as err only subject to that data type implementing the error interface.
There are some really good blogs about how to do error handling:
The first one gives you a general overview of how to handle errors. The second one by Rob Pike tells you how errors are actually values so you can compare them like you would compare any 2 values. Simple enough and makes inspection very straightforward.
In addition to all of this Go also has a standard library package errors which let you wrap, unwrap, compare (in chain), and convert errors. To know more about that check out this package.
What’s Missing?
When I started using Go in a startup that I co-founded one thing that bothered me a lot was how everyone who used Go used the error’s Error() method to check the type of error based on the string this method returned. It was usually something of this sort.
// This is what rob pike refers to in his blog for errors are values
if err != nil {
return err
}
// OR
// One more commonly seen handling was this
if err != nil {
if err.Error() == "invalid parameter" {
// Do some handling here
fmt.Println("something went wrong " + err.Error())
}
// default case
fmt.Println("something went wrong " + err.Error())
}
I felt this was something that needed to change and having come from a C/C++ background where we used pre-processor define macros (it was like black magic) to define errors and return a code that could help you handle your errors better. This paradigm again is actually based on the linux errno ISO C standard.
That being said the summary of the problem is below
- Errors are values but we aren't motivating developers to use them that way.
- Since Go is powerful and extensible I wanted a way to capture and store as much information as possible in the error which was too cumbersome which is why developers shied away from defining error structs when writing packages. (Some good packages out there do put the effort to define clear error codes and structs)
Back to Basics
Before I get to how this can be simplified, I’ll go through my thought process on what an error is supposed to represent and what properties it should possess.
What is an error supposed to represent?
An error is supposed to represent a condition that indicates the non-completion or partial completion of a task or action that was intended to be completed.
What properties should an error possess?
As per my understanding, 3 key properties of an error are the following
- Clear — The error you throw should be clear and concise. It should not lead to any confusion (one such case would be not defining enough errors and just returning something that went wrong).
- Actionable — What needs to be done to mitigate this error needs to be communicated clearly.
- Contextual — It helps to add contextual information when returning an error. Revealing an underlying error that may require inspection is one of them.
Some good to haves…
- Easy Inspection — One should be able to easily inspect an error and handle them.
- Formatting helpers — Since errors may be logged you may want to provide formatters for the error so that they are easy to read.
And Now the Solution (My take on this problem)
To begin with the original paradigm of being able to define error codes as constants in C land I decided, one must be able to define similar constants in go.
type MyErrorCodeType int
const (
NotFound MyErrorCodeType = 100 + iota // I didn't find what you were looking for!
AlreadyExists // I have this already!
Internal // Its not you, its me :(
Unknown // I don't know what happened
Busy // I'm busy rn, can we do this later?
Unauthorised // You ain't got the creds to do this
Fatal // Help!!! Im dying!!!
)
Now we can use these items as errors as long as we implement the error interface for the type MyErrorCodeType
. Which could be something like this:
func (i MyErrorCodeType) Error() string {
return fmt.Sprintf("error: %d", i)
}
Now this is a great start. Errors are now clear and actionable. They can be used directly as values for easy inspection. The contextual property can be satisfied to some extent by using the go builtin error wrapping but no additional context is available. Also, the main issue is when logging these errors we would need to have a lookup table to see which error code refers to which error.
During my initial days of working on Go, I came across stringer which helps you print names of the constants or any comments annotated with it. This was an interesting tool with great potential.
What if we could use the power of the stringer tool to print out/log our error constants but also have a comprehensive way to add context and also be able to compare errors? That's what led me to write this Go library with a generator tool called ohno.
More about this in the next part of this series.