•••
I wrote an article a while back on implementing custom handler types to avoid a few common problems with the existing http.HandlerFunc
—the func MyHandler(w http.ResponseWriter, r *http.Request)
signature you often see. It’s a useful “general purpose” handler type that covers the basics, but—as with anything generic—there are a few shortcomings:
- Having to remember to explicitly call a naked
return
when you want to stop processing in the handler. This is a common case when you want to raise a re-direct (301/302), not found (404) or internal server error (500) status. Failing to do so can be the cause of subtle bugs (the function will continue) and because the function signature doesn’t require a return value, the compiler won’t alert you. - You can’t easily pass in additional arguments (i.e. database pools, configuration values). You end up having to either use a bunch of globals (not terrible, but tracking them can scale poorly) or stash those things into a request context and then type assert each of them out. Can be clunky.
- You end up repeating yourself. Want to log the error returned by your DB package? You can either call
log.Printf
in your database package (in each query func), or in every handler when an error is returned. It’d be great if your handlers could just return that to a function that centrally logs errors and raise a HTTP 500 on the ones that call for it.
My previous approach used the func(http.ResponseWriter, *http.Request) (int, error)
signature. This has proven to be pretty neat, but a quirk is that returning “non error” status codes like 200, 302, 303 was often superfluous—you’re either setting it elsewhere or it’s effectively unused — e.g.
func SomeHandler(w http.ResponseWriter, r *http.Request) (int, error) {
db, err := someDBcall()
if err != nil {
// This makes sense.
return 500, err
}
if user.LoggedIn {
http.Redirect(w, r, "/dashboard", 302)
// Superfluous! Our http.Redirect function handles the 302, not
// our return value (which is effectively ignored).
return 302, nil
}
}
It’s not terrible, but we can do better.
A Little Different
So how can we improve on this? Let’s lay out some code:
package handler
// Error represents a handler error. It provides methods for a HTTP status
// code and embeds the built-in error interface.
type Error interface {
error
Status() int
}
// StatusError represents an error with an associated HTTP status code.
type StatusError struct {
Code int
Err error
}
// Allows StatusError to satisfy the error interface.
func (se StatusError) Error() string {
return se.Err.Error()
}
// Returns our HTTP status code.
func (se StatusError) Status() int {
return se.Code
}
// A (simple) example of our application-wide configuration.
type Env struct {
DB *sql.DB
Port string
Host string
}
// The Handler struct that takes a configured Env and a function matching
// our useful signature.
type Handler struct {
*Env
H func(e *Env, w http.ResponseWriter, r *http.Request) error
}
// ServeHTTP allows our Handler type to satisfy http.Handler.
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := h.H(h.Env, w, r)
if err != nil {
switch e := err.(type) {
case Error:
// We can retrieve the status here and write out a specific
// HTTP status code.
log.Printf("HTTP %d - %s", e.Status(), e)
http.Error(w, e.Error(), e.Status())
default:
// Any error types we don't specifically look out for default
// to serving a HTTP 500
http.Error(w, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
}
}
}
The code above should be self-explanatory, but to clarify any outstanding points:
- We create a custom
Error
type (an interface) that embeds Go’s built-in error interface and also has aStatus() int
method. - We provide a simple
StatusError
type (a struct) that satisfies ourhandler.Error
type. Our StatusError type accepts a HTTP status code (an int) and an error that allows us to wrap the root cause for logging/inspection. - Our
ServeHTTP
method contains a type switch—which is thee := err.(type)
part that tests for the errors we care about and allows us to handle those specific cases. In our example that’s just thehandler.Error
type. Other error types—be they from other packages (e.g.net.Error
) or additional error types we have defined—can also be inspected (if we care about their details).
If we don’t want to inspect them, our default
case catches them. Remember that the ServeHTTP
method allows our Handler
type to satisfy the http.Handler
interface and be used anywhere http.Handler is accepted: Go’s net/http package and all good third party frameworks. This is what makes custom handler types so useful: they’re flexible about where they can be used.
Note that the net
package does something very similar. It has a net.Error
interface that embeds the built-in error
interface and then a handful of concrete types that implement it. Functions return the concrete type that suits the type of error they’re returning (a DNS error, a parsing error, etc). A good example would be defining a DBError
type with a Query() string
method in a ‘datastore’ package that we can use to log failed queries.
Full Example
What does the end result look like? And how would we split it up into packages (sensibly)?
package handler
import (
"net/http"
)
// Error represents a handler error. It provides methods for a HTTP status
// code and embeds the built-in error interface.
type Error interface {
error
Status() int
}
// StatusError represents an error with an associated HTTP status code.
type StatusError struct {
Code int
Err error
}
// Allows StatusError to satisfy the error interface.
func (se StatusError) Error() string {
return se.Err.Error()
}
// Returns our HTTP status code.
func (se StatusError) Status() int {
return se.Code
}
// A (simple) example of our application-wide configuration.
type Env struct {
DB *sql.DB
Port string
Host string
}
// The Handler struct that takes a configured Env and a function matching
// our useful signature.
type Handler struct {
*Env
H func(e *Env, w http.ResponseWriter, r *http.Request) error
}
// ServeHTTP allows our Handler type to satisfy http.Handler.
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := h.H(h.Env, w, r)
if err != nil {
switch e := err.(type) {
case Error:
// We can retrieve the status here and write out a specific
// HTTP status code.
log.Printf("HTTP %d - %s", e.Status(), e)
http.Error(w, e.Error(), e.Status())
default:
// Any error types we don't specifically look out for default
// to serving a HTTP 500
http.Error(w, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
}
}
}
func GetIndex(env *Env, w http.ResponseWriter, r *http.Request) error {
users, err := env.DB.GetAllUsers()
if err != nil {
// We return a status error here, which conveniently wraps the error
// returned from our DB queries. We can clearly define which errors
// are worth raising a HTTP 500 over vs. which might just be a HTTP
// 404, 403 or 401 (as appropriate). It's also clear where our
// handler should stop processing by returning early.
return StatusError{500, err}
}
fmt.Fprintf(w, "%+v", users)
return nil
}
… and in our main package:
package main
import (
"net/http"
"github.com/you/somepkg/handler"
)
func main() {
db, err := sql.Open("connectionstringhere")
if err != nil {
log.Fatal(err)
}
// Initialise our app-wide environment with the services/info we need.
env := &handler.Env{
DB: db,
Port: os.Getenv("PORT"),
Host: os.Getenv("HOST"),
// We might also have a custom log.Logger, our
// template instance, and a config struct as fields
// in our Env struct.
}
// Note that we're using http.Handle, not http.HandleFunc. The
// latter only accepts the http.HandlerFunc type, which is not
// what we have here.
http.Handle("/", handler.Handler{env, handler.GetIndex})
// Logs the error if ListenAndServe fails.
log.Fatal(http.ListenAndServe(":8000", nil))
}
In the real world, you’re likely to define your Handler
and Env
types in a separate file (of the same package) from your handler functions, but I’ve keep it simple here for the sake of brevity. So what did we end up getting from this?
- A practical
Handler
type that satisfieshttp.Handler
can be used with
frameworks like net/http, gorilla/mux,
Goji and any others that sensibly accept ahttp.Handler
type. - Clear, centralised error handling. We inspect the errors we want to handle
specifically—ourhandler.Error
type—and fall back to a default
for generic errors. If you’re interested in better error handling practices in Go,
read Dave Cheney’s blog post,
which dives into defining package-levelError
interfaces. - A useful application-wide “environment” via our
Env
type. We don’t have to
scatter a bunch of globals across our applications: instead we define them in
one place and pass them explicitly to our handlers.
If you have questions about the post, drop me a line via @elithrar on Twitter,
or the Gopher community on Slack.
Posted on 02 July 2015
Nate Finch had a nice blog post on error flags
recently, and it caused me to think about error handling in my own
greenfield Go project at work.
Much of the Go software I write follows a common pattern: an HTTP JSON
API fronting some business logic, backed by a data store of some sort.
When an error occurs, I typically want to present a context-aware HTTP
status code and an a JSON payload containing an error message. I want
to avoid 400 Bad Request and 500 Internal Server Errors whenever
possible, and I also don’t want to expose internal implementation
details or inadvertently leak information to API consumers.
I’d like to share the pattern I’ve settled on for this type of application.
An API-safe error interface
First, I define a new interface that will be used throughout the
application for exposing “safe” errors through the API:
package app
type APIError interface {
// APIError returns an HTTP status code and an API-safe error message.
APIError() (int, string)
}
Common sentinel errors
In practice, most of the time there are a limited set of errors that I
want to return through the API. Things like a 401 Unauthorized for a
missing or invalid API token, or a 404 Not Found when referring to a
resource that doesn’t exist in the data store. For these I create a
create a private struct
that implements APIError
:
type sentinelAPIError struct {
status int
msg string
}
func (e sentinelAPIError) Error() string {
return e.msg
}
func (e sentinelAPIError) APIError() (int, string) {
return e.status, e.msg
}
And then I publicly define common sentinel errors:
var (
ErrAuth = &sentinelAPIError{status: http.StatusUnauthorized, msg: "invalid token"}
ErrNotFound = &sentinelAPIError{status: http.StatusNotFound, msg: "not found"}
ErrDuplicate = &sentinelAPIError{status: http.StatusBadRequest, msg: "duplicate"}
)
Wrapping sentinels
The sentinel errors provide a good foundation for reporting basic
information through the API, but how can I associate real errors with
them? ErrNoRows
from the database/sql
package is never going to
implement my APIError
interface, but I can leverage the
error wrapping functionality introduced in Go 1.13.
One of the lesser-known features of error wrapping is the ability to
write a custom Is
method on your own types. This is perhaps because
the implementation is privately hidden
within the errors
package, and the package documentation
doesn’t give much information about why you’d want to use it. But it’s
a perfect fit for these sentinel errors.
First, I define a sentinel-wrapped error type:
type sentinelWrappedError struct {
error
sentinel *sentinelAPIError
}
func (e sentinelWrappedError) Is(err error) bool {
return e.sentinel == err
}
func (e sentinelWrappedError) APIError() (int, string) {
return e.sentinel.APIError()
}
This associates an error from elsewhere in the application with one of
my predefined sentinel errors. A key thing to note here is that
sentinelWrappedError
embeds the original error, meaning its Error
method returns the original error’s message, while implementing
APIError
with the sentinel’s API-safe message. The Is
method allows
for comparisons of these wrapping errors with the sentinel errors using
errors.Is
.
Then I need a public function to do the wrapping:
func WrapError(err error, sentinel *sentinelAPIError) error {
return sentinelWrappedError{error: err, sentinel: sentinel}
}
(If you wanted to include additional context in the APIError
, such as a resource name, this would be a good place to add it.)
When other parts of the application encounter an error, they wrap the
error with one of the sentinel errors. For example, the database layer
might have its own wrapError
function that looks something like this:
package db
import "example.com/app"
func wrapError(err error) error {
switch {
case errors.Is(err, sql.ErrNoRows):
return app.WrapError(err, app.ErrNotFound)
case isMySQLError(err, codeDuplicate):
return app.WrapError(err, app.ErrDuplicate)
default:
return err
}
}
Because the wrapper implements Is
against the sentinel, you can
compare errors to sentinels regardless of what the original error is:
err := db.DoAThing()
switch {
case errors.Is(err, ErrNotFound):
// do something specific for Not Found errors
case errors.Is(err, ErrDuplicate):
// do something specific for Duplicate errors
}
Handling errors in the API
The final task is to handle these errors and send them safely back
through the API. In my api
package, I define a helper function that
takes an error and serializes it to JSON:
package api
import "example.com/app"
func JSONHandleError(w http.ResponseWriter, err error) {
var apiErr app.APIError
if errors.As(err, &apiErr) {
status, msg := apiErr.APIError()
JSONError(w, status, msg)
} else {
JSONError(w, http.StatusInternalServerError, "internal error")
}
}
(The elided JSONError
function is the one responsible for setting the
HTTP status code and serializing the JSON.)
Note that this function can take any error
. If it’s not an
APIError
, it falls back to returning a 500 Internal Server Error.
This makes it safe to pass unwrapped and unexpected errors without
additional care.
Because sentinelWrappedError
embeds the original error, you can also
log any error you encounter and get the original error message. This
can aid debugging.
An example
Here’s an example HTTP handler function that generates an error, logs
it, and returns it to a caller.
package api
func exampleHandler(w http.ResponseWriter, r *http.Request) {
// A contrived example that always throws an error. Imagine this
// is actually a function that calls into a data store.
err := app.WrapError(fmt.Errorf("user ID %q not found", "archer"), app.ErrNotFound)
if err != nil {
log.Printf("exampleHandler: error fetching user: %v", err)
JSONHandleError(w, err)
return
}
// Happy path elided...
}
Hitting this endpoint will give you this HTTP response:
HTTP/1.1 404 Not Found
Content-Type: application/json
{"error": "not found"}
And send to your logs:
exampleHandler: error fetching user: user ID "archer" not found
If I had forgotten to call app.WrapError
, the response instead would
have been:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{"error": "internal error"}
But the message to the logs would have been the same.
Impact
Adopting this pattern for error handling has reduced the number of error
types and scaffolding in my code – the same problems that Nate
experienced before adopting his error flags scheme. It’s centralized
the errors I expose to the user, reduced the work to expose appropriate
and consistent error codes and messages to API consumers, and has an
always-on safe fallback for unexpected errors or programming mistakes.
I hope you can take inspiration to improve the error handling in your
own code.
Introduction
This guide explains error handling in Go and the best practices for handling API errors in Go. You should have a working knowledge of Go and understand how web APIs work to get the most benefit from this article.
Go has a unique approach to handling errors. Rather than utilizing a try..catch block like other programming languages such as C++, Go treats errors as first-class values with panic and recovery mechanisms.
Go has a built-in type called error, which exposes an interface that implements the Error() method:
type error interface {
Error() string
}
Typically the usual way to handle errors in Go is to check if the returned error value is nil. If it’s equal to nil, then it means no errors occurred.
Go functions can also return multiple values. In cases where a function can fail, it’s a good idea to return the error status as a second return value. If the function doesn’t return anything, you should return the error status.
func myFunc() error {
// do something
}
err := myFunc()
if err != nil {
// do something
}
You can define errors using the errors.New() function, and have it printed out to the console using the Error() method:
func myFunc() error {
myErr := errors.New(My Errorâ€)
return myErr
}
err := myFunc()
if err != nil {
fmt.Println(err.Error())
}
This prints our defined error(My Error») to the screen.
The panic and recovery mechanisms work a bit differently. As panic halts program flow and causes the execution to exit with a non-zero status code, and then prints out the stack trace and error.
if err != nil {
panic(err)
}
When a function calls panic(), it won’t be executed further, but all deferred functions will be called. Recover is typically used with this defer mechanism to rescue the program from the panic.
func panicAndRecover() {
defer func() {
if err := recover(); err != nil {
fmt.Println(Successfully recovered from panicâ€)
}
}()
panic(Panicking")
}
When the panicAndRecover function is called, it panics, but rather than returning a non-zero exit status and exiting, it runs the deferred anonymous function first. This recovers from the panic using recover(), and prints out to the screen. Normal program execution occurs without exiting immediately as we successfully recover from panic.
API Error Handling
When building web APIs, appropriately handling errors is an integral part of the development process. Examples of such errors include JSON parsing errors, wrong endpoint requests, etc.
Let’s dive into some of the best practices for handling API errors in Go:
Using Appropriate HTTP Status Codes
HTTP status codes communicate the status of an HTTP request; you must carefully return status codes representing the state of a request.
HTTP status codes are split into five categories:
-
1xx — Information
-
2xx — Success
-
3xx — Redirection
-
4xx — Client error
-
5xx — Server error
Many developers using your API might rely solely on the status code to see if the request was successful. Sending a 200 (success) code followed by an error is bad practice. Instead, it is proper to return a more appropriate code, such as 400 (bad request).
For instance, consider the following reqChecker function, which takes a request body and returns nil or some error value, depending on the condition of the request body. An http.StatusOK
(200 status code) is misleading if the request body doesn’t conform to our standard.
func checker(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
if err := reqChecker(r.Body); err != nil {
fmt.Fprintf(w, Invalid request body error: %sâ€, err.Error())
}
}
We can refactor the handler to return a more appropriate error code:
func checker(w http.ResponseWriter, r *http.Request) {
if err := reqChecker(r.Body); err != nil {
w.WriteHeader(http.StatusBadRequest) // 400 http status code
fmt.Fprintf(w, Invalid request body error:%sâ€, err.Error())
} else {
w.writeHeader(http.StatusOK)
}
}
There are quite a large number of HTTP status codes available, but just a subset of them are actually used in practice, with the most common ones being:
-
200 — Ok
-
201 — Created
-
304 — Not modified
-
400 — Bad Request
-
401 — Unauthorized
-
403 — Forbidden
-
404 — Not Found
-
500 — Internal Server Error
-
503 — Service unavailable
It’s not ideal to use too many status codes in your application. Keeping it to a minimum is recommended practice.
Descriptive Error Messages
While sending proper HTTP status error codes is a very important step in handling API errors, returning descriptive messages provides the client with additional information.
Multiple errors could return the same error code. For instance, posting a wrong JSON request format and malformed requests could spawn the same http.StatusBadRequest
error (status code 400). The status code indicates that the request failed due to the client’s error but didn’t provide much about the nature of the error.
Returning a JSON response to the client alongside the error code like the following is more descriptive:
{
"error": "Error parsing JSON request",
"message": "Invalid JSON format",
"detail": "Post the 'author' and 'id' fields in the request body."
}
Note: Descriptive error messages should be framed carefully not to expose the inner workings of the API to attackers.
The error field within the response should be unique across your application, and the detail field should provide more information about the error or how to fix it.
Avoid generic error messages, as it’s much better to know why specifically the request failed rather than a generic error code.
Exhaustive Documentation
Documentation is an essential part of API development. While API errors can be handled with ease by formulating proper responses when errors occur, comprehensive documentation helps the clients know all the relevant information concerning your API, such as what endpoints are provided by your API, the response and request formats, parameter options, and more.
The more exhaustive your documentation, the less likely clients will spend time battling API errors.
Conclusion
In this guide, we have learned about error handling in Go and some of the best practices for handling API errors.
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.
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 ….
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.
The Go BlogSearchAndrew Gerrand 12 July 2011
In this article, we’ll take a look at how to handle errors using build-in Golang functionality, how you can extract information from the errors you are receiving and the best practices to do so.
Error handling in Golang is unconventional when compared to other mainstream languages like Javascript, Java and Python. This can make it very difficult for new programmers to grasp Golangs approach of tackling error handling.
In this article, we’ll take a look at how to handle errors using build-in Golang functionality, how you can extract information from the errors you are receiving and the best practices to do so. A basic understanding of Golang is therefore required to follow this article. If you are unsure about any concepts, you can look them up here.
Errors in Golang
Errors indicate an unwanted condition occurring in your application. Let’s say you want to create a temporary directory where you can store some files for your application, but the directory’s creation fails. This is an unwanted condition and is therefore represented using an error.
package main
import (
"fmt"
"ioutil"
)
func main() {
dir, err := ioutil.TempDir("", "temp")
if err != nil {
return fmt.Errorf("failed to create temp dir: %v", err)
}
}
Golang represents errors using the built-in error type, which we will look at closer in the next section. The error is often returned as a second argument of the function, as shown in the example above. Here the TempDir function returns the name of the directory as well as an error variable.
Creating custom errors
As already mentioned errors are represented using the built-in error interface type, which has the following definition:
type error interface {
Error() string
}
The interface contains a single method Error() that returns an error message as a string. Every type that implements the error interface can be used as an error. When printing the error using methods like fmt.Println the Error() method is automatically called by Golang.
There are multiple ways of creating custom error messages in Golang, each with its own advantages and disadvantages.
String-based errors
String-based errors can be created using two out-of-the-box options in Golang and are used for simple errors that just need to return an error message.
err := errors.New("math: divided by zero")
The errors.New() method can be used to create new errors and takes the error message as its only parameter.
err2 := fmt.Errorf("math: %g cannot be divided by zero", x)
fmt.Errorf on the other hand also provides the ability to add formatting to your error message. Above you can see that a parameter can be passed which will be included in the error message.
Custom error with data
You can create your own error type by implementing the Error() function defined in the error interface on your struct. Here is an example:
type PathError struct {
Path string
}
func (e *PathError) Error() string {
return fmt.Sprintf("error in path: %v", e.Path)
}
The PathError implements the Error() function and therefore satisfies the error interface. The implementation of the Error() function now returns a string with the path of the PathError struct. You can now use PathError whenever you want to throw an error.
Here is an elementary example:
package main
import(
"fmt"
)
type PathError struct {
Path string
}
func (e *PathError) Error() string {
return fmt.Sprintf("error in path: %v", e.Path)
}
func throwError() error {
return &PathError{Path: "/test"}
}
func main() {
err := throwError()
if err != nil {
fmt.Println(err)
}
}
You can also check if the error has a specific type using either an if or switch statement:
if err != nil {
switch e := err.(type) {
case *PathError :
// Do something with the path
default:
log.Println(e)
}
}
This will allow you to extract more information from your errors because you can then call all functions that are implemented on the specific error type. For example, if the PathError had a second method called GetInfo you could call it like this.
e.GetInfo()
Error handling in functions
Now that you know how to create your own custom errors and extract as much information as possible from errors let’s take a look at how you can handle errors in functions.
Most of the time errors are not directly handled in functions but are returned as a return value instead. Here we can take advantage of the fact that Golang supports multiple return values for a function. Thus you can return your error alongside the normal result — errors are always returned as the last argument — of the function as follows:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0.0, errors.New("cannot divide through zero")
}
return a/b, nil
}
The function call will then look similar to this:
func main() {
num, err := divide(100, 0)
if err != nil {
fmt.Printf("error: %s", err.Error())
} else {
fmt.Println("Number: ", num)
}
}
If the returned error is not nil it usually means that there is a problem and you need to handle the error appropriately. This can mean that you use some kind of log message to warn the user, retry the function until it works or close the application entirely depending on the situation. The only drawback is that Golang does not enforce handling the retuned errors, which means that you could just ignore handling errors completely.
Take the following code for example:
package main
import (
"errors"
"fmt"
)
func main() {
num2, _ := divide(100, 0)
fmt.Println("Number: ", num2)
}
The so-called blank identifier is used as an anonymous placeholder and therefore provides a way to ignore values in an assignment and avoid compiler errors in the process. But remember that using the blank identifier instead of probably handling errors is dangerous and should not be done if it can be avoided.
Defer, panic and recover
Go does not have exceptions like many other programming languages, including Java and Javascript but has a comparable mechanism know as ,,Defer, panic and recover». Still the use-cases of panic and recover are very different from exceptions in other programming languages as they should only be used in unexpected and unrecoverable situations.
Defer
A defer statement is a mechanism used to defer a function by putting it into an executed stack once the function that contains the defer statement has finished, either normally by executing a return statement or abnormally panicking. Deferred functions will then be executed in reverse order in which they were deferred.
Take the following function for example:
func processHTML(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
resp.Body.Close()
return fmt.Errorf("%s has content type %s which does not match text/html", url, ct)
}
doc, err := html.Parse(resp.Body)
resp.Body.Close()
// ... Process HTML ...
return nil
}
Here you can notice the duplicated resp.Body.Close call, which ensures that the response is properly closed. Once functions grow more complex and have more errors that need to be handled such duplications get more and more problematic to maintain.
Since deferred calls get called once the function has ended, no matter if it succeeded or not it can be used to simplify such calls.
func processHTMLDefer(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
ct := resp.Header.Get("Content-Type")
if ct != "text/html" && !strings.HasPrefix(ct, "text/html;") {
return fmt.Errorf("%s has content type %s which does not match text/html", url, ct)
}
doc, err := html.Parse(resp.Body)
// ... Process HTML ...
return nil
}
All deferred functions are executed in reverse order in which they were deferred when the function finishes.
package main
import (
"fmt"
)
func main() {
first()
}
func first() {
defer fmt.Println("first")
second()
}
func second() {
defer fmt.Println("second")
third()
}
func third() {
defer fmt.Println("third")
}
Here is the result of running the above program:
third
second
first
Panic
A panic statement signals Golang that your code cannot solve the current problem and it therefore stops the normal execution flow of your code. Once a panic is called, all deferred functions are executed and the program crashes with a log message that includes the panic values (usually an error message) and a stack trace.
As an example Golang will panic when a number is divided by zero.
package main
import "fmt"
func main() {
divide(5)
}
func divide(x int) {
fmt.Printf("divide(%d) n", x+0/x)
divide(x-1)
}
Once the divide function is called using zero, the program will panic, resulting in the following output.
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.divide(0x0)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:16 +0xe6
main.divide(0x1)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x2)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x3)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x4)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.divide(0x5)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:17 +0xd6
main.main()
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:11 +0x31
exit status 2
You can also use the built-in panic function to panic in your own programms. A panic should mostly only be used when something happens that the program didn’t expect and cannot handle.
func getArguments() {
if len(os.Args) == 1 {
panic("Not enough arguments!")
}
}
As already mentioned, deferred functions will be executed before terminating the application, as shown in the following example.
package main
import (
"fmt"
)
func main() {
accessSlice([]int{1,2,5,6,7,8}, 0)
}
func accessSlice(slice []int, index int) {
fmt.Printf("item %d, value %d n", index, slice[index])
defer fmt.Printf("defer %d n", index)
accessSlice(slice, index+1)
}
Here is the output of the programm:
item 0, value 1
item 1, value 2
item 2, value 5
item 3, value 6
item 4, value 7
item 5, value 8
defer 5
defer 4
defer 3
defer 2
defer 1
defer 0
panic: runtime error: index out of range [6] with length 6
goroutine 1 [running]:
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x6)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:29 +0x250
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x5)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x4)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x3)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x2)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x1)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.accessSlice(0xc00011df48, 0x6, 0x6, 0x0)
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:31 +0x1eb
main.main()
C:/Users/gabriel/articles/Golang Error handling/Code/panic/main.go:9 +0x99
exit status 2
Recover
In some rare cases panics should not terminate the application but be recovered instead. For example, a socket server that encounters an unexpected problem could report the error to the clients and then close all connections rather than leaving the clients wondering what just happened.
Panics can therefore be recovered by calling the built-in recover function within a deferred function in the function that is panicking. Recover will then end the current state of panic and return the panic error value.
package main
import "fmt"
func main(){
accessSlice([]int{1,2,5,6,7,8}, 0)
}
func accessSlice(slice []int, index int) {
defer func() {
if p := recover(); p != nil {
fmt.Printf("internal error: %v", p)
}
}()
fmt.Printf("item %d, value %d n", index, slice[index])
defer fmt.Printf("defer %d n", index)
accessSlice(slice, index+1)
}
As you can see after adding a recover function to the function we coded above the program doesn’t exit anymore when the index is out of bounds by recovers instead.
Output:
item 0, value 1
item 1, value 2
item 2, value 5
item 3, value 6
item 4, value 7
item 5, value 8
internal error: runtime error: index out of range [6] with length 6defer 5
defer 4
defer 3
defer 2
defer 1
defer 0
Recovering from panics can be useful in some cases, but as a general rule you should try to avoid recovering from panics.
Error wrapping
Golang also allows errors to wrap other errors which provides the functionality to provide additional context to your error messages. This is often used to provide specific information like where the error originated in your program.
You can create wrapped errors by using the %w flag with the fmt.Errorf function as shown in the following example.
package main
import (
"errors"
"fmt"
"os"
)
func main() {
err := openFile("non-existing")
if err != nil {
fmt.Printf("error running program: %s n", err.Error())
}
}
func openFile(filename string) error {
if _, err := os.Open(filename); err != nil {
return fmt.Errorf("error opening %s: %w", filename, err)
}
return nil
}
The output of the application would now look like the following:
error running program: error opening non-existing: open non-existing: no such file or directory
As you can see the application prints both the new error created using fmt.Errorf as well as the old error message that was passed to the %w flag. Golang also provides the functionality to get the old error message back by unwrapping the error using errors.Unwrap.
package main
import (
"errors"
"fmt"
"os"
)
func main() {
err := openFile("non-existing")
if err != nil {
fmt.Printf("error running program: %s n", err.Error())
// Unwrap error
unwrappedErr := errors.Unwrap(err)
fmt.Printf("unwrapped error: %v n", unwrappedErr)
}
}
func openFile(filename string) error {
if _, err := os.Open(filename); err != nil {
return fmt.Errorf("error opening %s: %w", filename, err)
}
return nil
}
As you can see the output now also displays the original error.
error running program: error opening non-existing: open non-existing: no such file or directory
unwrapped error: open non-existing: no such file or directory
Errors can be wrapped and unwrapped multiple times, but in most cases wrapping them more than a few times does not make sense.
Casting Errors
Sometimes you will need a way to cast between different error types to for example, access unique information that only that type has. The errors.As function provides an easy and safe way to do so by looking for the first error in the error chain that fits the requirements of the error type. If no match is found the function returns false.
Let’s look at the official errors.As docs example to better understand what is happening.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main(){
// Casting error
if _, err := os.Open("non-existing"); err != nil {
var pathError *os.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
}
Here we try to cast our generic error type to os.PathError so we can access the Path variable that that specific error contains.
Another useful functionality is checking if an error has a specific type. Golang provides the errors.Is function to do exactly that. Here you provide your error as well as the particular error type you want to check. If the error matches the specific type the function will return true, if not it will return false.
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main(){
// Check if error is a specific type
if _, err := os.Open("non-existing"); err != nil {
if errors.Is(err, fs.ErrNotExist) {
fmt.Println("file does not exist")
} else {
fmt.Println(err)
}
}
}
After checking, you can adapt your error message accordingly.
Sources
- Golang Blog — Working with Errors in Go 1.13
- The Go Programming language book
- Golang Blog — Defer, Panic, and Recover
- LogRocket — Error handling in Golang
- GolangByExample — Wrapping and Un-wrapping of error in Go
- Golang Documentation — Package errors
Conclusion
You made it all the way until the end! I hope this article helped you understand the basics of Go error handling and why it is an essential topic in application/software development.
If you have found this helpful, please consider recommending and sharing it with other fellow developers and subscribing to my newsletter. If you have any questions or feedback, let me know using my contact form or contact me on Twitter.