Part I: When Errors cause API failures

Alec develops a set of APIs and tests out their basic functionalities. They worked just fine. Great! He releases his APIs for others to use, calls it a day and starts daydreaming about a nice vacation by the beach.....

Meanwhile,  on the other side of the world, someone decides to use Alec's APIs. They don't really know much about his system and they hit one of the services with some random input.... and....the service breaks, the system shuts down.....mayday!!

Seems that Alec only tested out the ideal conditions and never really saw this coming....

Take my advice, never be like Alec. Never! When developing your APIs always remember that an error scenario will always arise. The user may send an incorrect request or there might be some errors within your logic that might have been overlooked. In any condition, your ultimate aim is to never let the system stop execution .

You need to think of various situations where your API can break and take mitigative measures. Even if an error occurs, you need to handle it gracefully and inform the user about the error.

In this post, we are going to be doing exactly that. And like our previous series, we will continue with the petstore application example ....

Error Handling in Go

In other programming languages, errors are mostly handled using "try","catch","except" blocks. Its pretty straightforward in Go though! Go has a built in type called "error" which basically looks like this.

type error interface {
    Error() string
}

In any case where an error may occur, the usual way to handle it is to check if the error returned is nil or not. If its nil, then that means the logic worked fine, else an error occurred.

func myFunc() error{
   //do something that may return an error
}

err := myFunc()
if err!=nil{
   //do something
}

The description of the error can be obtained by using the Error() function and printing it out

func myFunc() error{
   return errors.New("This is an error")
}

err := myFunc()
if err!=nil{
   fmt.Println(err.Error())
}

The panic() function is mostly used if you don't know what to do with the error. Panic will cause the execution to exit with a non zero status code and will print out the error and its stack trace

if err!=nil{
	panic(err)
}

Error Codes in HTTP

When using HTTP, there are specific codes which have been defined for errors. Know that your API may throw an error in two conditions:

  • Client Errors: When the Client side requests for invalid, missing or incorrect data
  • Server Errors: When the Server fails to furnish the client request because of some error that has occurred internally
All error codes of format 4XX are Client side errors and 5XX format belong to server errors.

Let's look at a few most commonly used error codes an analogy:

  • 400 - Bad Request
    This ones like asking for a pizza at an ice cream shop. That's a "bad request" !
    That means that the client has sent some request which cannot be understood by the server has to be modified.
  • 404 - Not Found
    You ask for a mint chocolate ice cream but the shop doesn't have mint chocolate. (See you asked for an ice cream in an ice cream shop so its not a "bad request", but the flavour you wanted was "not found")
    This means that the client has asked for some data which the server couldn't find.
  • 500 - Internal Server Error
    You asked for an ice cream and the ice cream shop does have the flavour. But the vendor tells you can't get the ice cream because there is some problem in the kitchen. Its an "internal error" you see!
    This means there was some error in the server's function because of which you couldn't get the response back

Error Handling in Our API

At the end of our previous series, we had a sweet set of working APIs. We'd tested them out by sending the appropriate input and got the correct output accordingly. But what if we sent a wrong input?

Remember we have an endpoint GET "/category/{id}" where we pass the category identifier to get the details of the category. Now think of situations where this API can break. We are listing down a few

  • The identifier is supposed to be an integer. What if we pass a string value?
  • We pass an identifier but there's no such record not present in the database.

In our current situation let's try out one of these situations.

Try calling the API with id="hello". Another way is to set id=10 which doesn't exist in our data. When calling from Postman, this is what you'll see:

The API returns no response and any external user will have no clue as to why this happend ! But since you are the developer, you can easily see the logs:

See? It clearly says there are no records for this. We should definitely tell that to the user, shouldn't we?

Let's do that now. Take a look at the current handler function for the endpoint which is the GetCategory() function within internal/petstore/rest/category.go file:

//GetCategory get a single category
func GetCategory(c echo.Context) error {
	// Get id from the path param for category. eg /category/1
	id, err := strconv.Atoi(c.Param("id"))
	// Call category service to get category
	response, err := service.GetCategory(id)
	if err != nil {
		panic(err)
	}
	// send JSON response
	return c.JSON(http.StatusOK, response)

}

We will slightly modify this to handle our error.

Returning errors using Echo

Echo framework provides error handling HTTPError type which can be returned in the response. The default error response returned is:

{
"message":"<Your error string>"
}

The error can be returned by using echo.NewHTTPError().

return echo.NewHTTPError(<statuscode>,<error string>)

Using this, let's handle each of the situations we mentioned above.

When an incorrect input value is sent

We are passing the value "hello" to the "id" path parameter in the GET /category/{id} endpoint. In this case, id should be an integer. Let's look at the first

In our code, we are converting the id path parameter to integer using "strconv.Atoi()". If the value passed cannot be converted, then an error is thrown. In this case, we'll return an error response with the status as "BadRequest" with code 400 since the type of value passed is incorrect.

//GetCategory get a single category
func GetCategory(c echo.Context) error {
	// Get id from the path param for category. eg /category/1
	id, err := strconv.Atoi(c.Param("id"))
    
   	//-- ADD ERROR HANDLING HERE --
    if err != nil {
	return echo.NewHTTPError(http.StatusBadRequest, "id sent is incorrect.Please send a valid integer value")
	}
    
    .....
    }

Lets see how the API response looks now:

When a record corresponding to the input value doesn't exist

We are passing the value 10 to the "id" path parameter in the GET /category/{id} endpoint but a record for category with id=10 doesn't exist.  To be honest, its not an error exactly. The user doesn't really know what records you database has. And if the record isn't there  we are bound to get an empty response.

In such a case, we have to pass a response with the status as  "NotFound" and code 404.  The error will be sent if the response object returned in your code  is empty or "nil".

//GetCategory get a single category
func GetCategory(c echo.Context) error {
	// Get id from the path param for category. eg /category/1
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "id sent is incorrect. Please send a valid integer value")
	}
	// Call category service to get category
	response, err := service.GetCategory(id)
	if response == nil {
		return echo.NewHTTPError(http.StatusNotFound, "No records found for given category id ")
	}
	....

}

Lets look at how this scenario looks like :

Returning internal errors

While returning any other error, we can return a response with status as "InternalServerError" with code 500.

if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failure in fetching category for the given id")
	}

Overall, your entire function for the get category for an id will look like:

//GetCategory get a single category
func GetCategory(c echo.Context) error {
	// Get id from the path param for category. eg /category/1
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "id sent is incorrect. Please send a valid integer value")
	}
	// Call category service to get category
	response, err := service.GetCategory(id)
	if response == nil {
		return echo.NewHTTPError(http.StatusNotFound, "No records found for given category id ")
	}
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, "Failure in fetching category for the given id")
	}
	// send JSON response
	return c.JSON(http.StatusOK, response)

}

Perfect! One of your APIs is now error proof! In a similar manner, you can handle all the errors in all the API handlers present under the /internal/petstore/rest folder.

In Summary

Although it seems trivial, error handling is always a crucial aspect while developing any software since it makes the system robust.

Your system is like a black box for the external users and APIs are the only way with which they can interact with the system. If your API response doesn't return appropriate information in case of errors, the user will remain clueless and your accountability will decrease. So never forget to find and catch those errors!

References:

Error Handling | Echo - High performance, minimalist Go web framework
Error handling in Echo | Echo is a high performance, extensible, minimalist web framework for Go (Golang).
Error handling and Go - The Go Blog
Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.