DEV Community

MAZEN Kenjrawi
MAZEN Kenjrawi

Posted on • Originally published at Medium

[Golang] Error Handling, the neat way!

I've seen lot's of developers in the golang community, especially who were lately moved to it because of its simplicity, making some quick judgment on how things should be treated in golang.

One of these things is error handling. As most of the language current devs are coming from OOP background, and they are used to deal with exceptions, or let's say the concept of "throwing" an exception to stop the current flow of the application, and they mostly think this is the way to go in golang too, we have to stop the flow of our application in case something went wrong. 
Well they are wrong!

Do not abuse your tools

I've seen it a lot and I used to do it as well. Just os.exit(1) whenever something unexpectedly happened and move on. Well, that's not the right way to go with go!

I can see why this is widely used, as most of the early golang applications were just terminal tools, and as so many of these tools used to be built using .sh executable where we used to just exit 1; to indicate that something unexpected just happened and we want to exit.

We carried this habbit along to our golang simple terminal applications and then to the complicated ones, it's just another cargo cult programming thing.

I heighly encourage you to do that very carefully in case you have to, as it is:

  • Very hard to maintain as your application grows.
  • Most important, it's impossible to unit test such code which obviously indicates its uncleanness.
  • Exiting in such way will prevent the execution of any deferred operations you have, your program will terminate immediately, and this may lead to resource leaks. Consider this for example:
func main() {
    dbConnection := db.connect("...")
    defer dbConnection.Close() // this operation won't be executed!
    entity := Entity{}
    err := dbConnection.Save(entity)
    if err != nil {
        os.Exist(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

Consider propagating your errors

Errors are just another type in golang, you have to use them to control the flow of program execution.

To do so, we have to propagate these errors throughout the program till the proper point of handling.

Consider an HTTP API for managing orders in which we want to forbbid the client from placing an order with particular conditions, for example:

package order
// package errors
var (
    UnableToShipToCountry = errors.New("unable to ship order to the requested country")
)
type Order struct {
    // ... order fields
}
type OrderRepo struct {
    DB db
    // ...
}
func newOrderFromRequest(o OrderRequest) (Order, error) {
    if o.ShippingAddress.Country != "DE" {
    return UnableToShipToCountry
    }
    // ... the creation logic
    return Order{...}, nil
}
func (r *OrderRepo)PlaceOrder(o OrderRequest) error {
    order, err := newOrderFromRequest(o)
    if err != nil {
        // don't handle the error here, its handling may differ
        return err
    }
    // ... db transaction may also return an error
    return r.db.Save(order)
}
Enter fullscreen mode Exit fullscreen mode

In our http handlers package:

package http
http.HandleFunc("/order", func (w http.ResponseWriter, r *http.Request) {
    orderRequest := createOrderRequest(r)
    err := orderRepo.PlaceOrder(orderRequest)
    if errors.Is(err, order.UnableToShipToCountry) {
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    if err != nil {
        // this error in case of DB transaction failure
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    // ...
    w.WriteHeader(http.StatusOK)

})
Enter fullscreen mode Exit fullscreen mode

Customize your errors

We can create our own customized error value and use it through out our program while consider adding some useful information to it, like error trace! Which may add a beneficial value to our logs, especially during debugging.

For example:

type AppErr struct {
    msg string
    code int
    trace string
}

func (e AppErr) Error() string {
    return fmt.Sprintf("Msg: %s, code: %d, trace:\n %s", e.msg, e.code, e.trace)
}

func NewAppErr(msg string, code int) AppErr {
    stackSlice := make([]byte, 512)
    s := runtime.Stack(stackSlice, false)

    return AppErr{msg, code, fmt.Sprintf("\n%s", stackSlice[0:s])}
}
Enter fullscreen mode Exit fullscreen mode

And we have such a use case inside an admin package:
package admin

func A() error {
    return b()
}

func b() error {
    return NewAppErr("error from b function!", 3)
}
And our main.go is:
func main() {
    err := admin.A()

    fmt.Println(err)
}
Enter fullscreen mode Exit fullscreen mode

The logged error message will be:

Msg: error from b function!, code: 3, trace:

goroutine 1 [running]:
./cmd/app/error.NewAppErr({0x1f42b0, 0x17}, 0x7)
        ./cmd/app/error/error.go:16 +0x35
./cmd/app/admin.b(...)
        ./cmd/app/admin/admin.go:12
./cmd/app/admin.A(...)
        ./cmd/app/admin/admin.go:8
main.main()
        ./cmd/app/main.go:10 +0x8d
Enter fullscreen mode Exit fullscreen mode

You can also consider shutting down your trace printing in prod or by checking another configuration value.

Hopefully this was helpful!

Top comments (0)