Error Handling in Golang

Muhammad Izzuddin Al Fikri
4 min readJul 1, 2024

--

Gift or curse?

Hello folks 👋

Most of the backbone services at Haraj are written using the Go programming language (which I will refer to as Go from now on). Please read here for the reasons. Personally, I only started using Go when I worked at Haraj. My previous experience was with PHP and JavaScript.

The first impression when working with Go is that it is a typed language. For someone who has only used dynamically typed languages, it is likely to feel frustrating because the thinking and paradigm are different. However, once you get the right way of thinking, everything will feel enjoyable.

Apart from Go’s programming language nature, which is different from PHP and JavaScript, there is one other significant difference: error handling. Initially, it felt tedious and bothersome. Why does almost every method need to handle errors independently? Many errors to be handled actually have the same behavior. It feels inefficient!

That was my first impression too, just like most (PHP & JavaScript developers) who are using Go for the first time. Over time, surrounded by great and supportive friends where I could learn and adapt quickly to using Go, I realized that error handling is one of Go’s cool features.

Here, I mentioned, “…many errors to be handled actually have the same behavior”. That means not all of them. Yes, and in reality, not all of them! There are many scenarios where we need to handle errors specially if we want the system to be robust.

Before I give some examples that strengthen my opinion, let’s see how error handling in Go works.

Suppose you are fetching data from the database, the code would be like this.

user, err := dbClient.Get(userID)
if err != nil {
log.Fatalf("unable to get user from database, due: %v", err)
}

You can see if the function returns an error, then we can react accordingly to the error. Let’s move on to the main topic, the reason why I say that error handling in Go is a gift.

TL;DR the main reason is that in terms of developer experience, the control over errors is very superior so that anyone reading our code can easily understand how the application we create works.

Here are some case examples I have found while working at Haraj related to error handling which I think really help in writing the application code we create.

1. Preventing our code from stopping when encountering negative conditions.

This is the most common case and is quite often implemented in Lambda functions that process data from the DynamoDB Stream. For example, we have a log table related to an event of a post in DynamoDB. The events include a newly created post, an updated post, and a deleted post. From the log table, on certain events, we will react by doing other things.

In DynamoDB, we can listen to the events using DynamoDB Stream and a Lambda function that will execute the code we want when the event occurs.

The problem is that every data in the stream will continue to be processed if our Lambda function does not returns error. If the function returns an error, the Lambda function will retried again with that erroneous data and of course most likely returns error again and that happens indefinitely. Thus, the stream will be blocked and new data will not be processed.

Well, sometimes we don’t need to ensure that all data must be processed correctly/without errors. With Go’s error handling behavior, this is very easy to implement. If an error occurs, we simply log the error for debugging purposes later and skip the erroneous data to continue processing the next data.

type Handler struct {}

func (h *Handler) Run(ctx context.Context, event events.DynamoDBEvent) {
for _, record := range event.Records {
data, err := h.repository.ProcessThatCanBeSkipped(ctx, record)
if err != nil {
// here we don't need to return the errors that caused our function won't process next data in the stream
log.Printf("error when processing data: %v", err)
continue
}
}
}

2. Reacting according to specific errors

There are times when we need special actions when specific errors occur. At Haraj, the services are deployed on AWS. When interacting with AWS services like S3, there are some specific errors that need to be handled with special needs.

For example, in the code below, if we want to get an object from S3 and the object is not there, indicated by the NoSuchKey error, the application will try to get the object from a different path.

pdfFile, err := s3Client.GetObjectWithContext(ctx, s3.GetObjectInput{
Bucket: aws.String("my-bucket"),
Key: aws.String("some/path/to/object.pdf")
})
if err != nil {
var aerr awserr.Error
if errors.As(err, &aerr && aerr.Code() == s3.ErrCodeNoSuchKey {
// put our custom logic if getting that kind of error
}

// we can put any custom logics for any specific error if we want

// at last, for any other errors, handled here
log.Printf("oops, something went wrong: %v", err)
}

You can see that reacting to specific errors here is easy, where the AWS SDK in Go has also prepared constants related to the errors in their service.

For most people, error handling in Go feels like a curse. If you are diligent, stay confident, and choose Go, then you will be grateful for error handling in Go.

Of course, the reasons I mentioned above can also be done in other programming languages. But what I want to emphasize again here is that I feel error handling in Go is verbose and very easy to maintain. I do not consider that error handling in other programming languages is bad, but in Go, it is more straightforward if we need special logic.

Keep coding and build for the better world, folks!

--

--