#Idiomatic error handling for a web service (CRUD REST API)

16 messages · Page 1 of 1 (latest)

vernal rover
#

Hi everyone!

In my team we are building a CRUD REST API and we hit, I guess, a quite basic issue which no one knows how to resolve. The issue is that we do not know how to property implement error handling in our app.

For simplicity, let's assume that we have 3 layers:

  • API layer - those are our HTTP handlers that call one or more of our "services"
  • Service layer - here resides the implementation logic.
  • Storage layer - code for accessing postgres/redis/etc
#

Currently, our error handling strategy is a complete mess:

  • in most places we just do
if err != nil{
    return err
}
  • in other places we do
if err != nil{
    return fmt.Errorf("bla bla %w, err)
}

or just

    if x > 5{
        return fmt.Errorf("something went wrong") // not wrapping another error
    }
  • and lastly, we have something inherited from another project, where we return more specific errors
return apperrors.NewBadRequestErr()
#

The problem with all those approaches, is that we cannot return proper HTTP status codes in our HTTP handler layer. How are we supposed to handle an error like fmt.Errorf("oops") that has been propagated from who knows where via if error != nil => return err ? Should we return HTTP 5XX, or HTTP 4XX ? How is the HTTP layer supposed to handle a PGX error, when it even should not be aware that such a thing as PGX even exists ?

In other languages like Java or Rust, we can just define an error type for the specific layer, for instance RepositoryException, or AuthorizationServiceError, etc and then we can easily map the lower level implementation specific error to a higher-level business error that does not leak implementation details. Unfortunately, in go errors are usually returned via the error type, so they are untyped, which makes properly handling them quite hard.

#

I guess I just have to unlearn my Java & Rust knowledge, so I'd like to know what's the idiomatic way to tackle that problem in Go. The goals I want to achieve are:

  • minimize boilerplate around error handling
  • a lower level layer should not leak implementation details
  • a lower level layer should know know how it's being used by an higher level layer
  • protect upper layers from changes in lower layers

Thanks!

next flame
#

well 4xx errors are client errors (bad request, file, method, etc), so you should be returning a 5xx instead, most likely 500 for internal server error

#

but for the implementation I'll let someone more seasoned reply 🙂

vernal rover
#

Yeah, we know when we want to return 4XX or 5XX, but the issue is that we cannot differentiate between the errors, because they can be something as simple as fmt.Errorf("oops") - so is this a 400 or 500 ? It's impossible to tell, because it does not have a proper type. ALso we do not want the lower levels to leak implementation details. So the HTTP layer should never receive lets say a pgx.Error or redis.Error, because those a re some impl details subject to change.

next flame
#

ahh well, I'll say it sounds like you may need to look into wrapping errors then? if your lower levels are returning an error and not what you requested presumably there's no recovery available, so I'd guess it's a 500 back to the client

vernal rover
#

Not necessarily. For instance PGX will return an error when a constraint is violated. A unique constraint violation should result in 409 conflict, whicle a CHECK constraint violation in 500.

#

So we can map those two errors to app specific errors, but then how to handle them in the upper layer : repo -> service -> api ?

#

Should we try to cast the error ?

#
repoErr, ok := err.(UniqError)
...
#

it's too much boilerplate if we wan to handle every possible error type

abstract basin
#

@vernal rover Have you looked into errors.Is? "Is reports whether any error in err's tree matches target"

You could use that to determine the root cause of the error and use that info to determine the appropriate status code to send back to the client.

https://pkg.go.dev/errors#Is

vernal rover
#

Thanks, I'll have to dig deeper into error wrapping. DO you have any tips how to structure the error hierarchy in a web app ?

abstract basin
#

You're welcome! Regarding error hierarchy, it should work itself out pretty well if you just make sure to return errors when you need to return errors.

As a part of that, I recommend you return a unique error from everywhere that can return an error, this extra context will aid in debugging what's happened by giving you a great way to trace the code execution.

The issue is described here under the "Don’t just check errors, handle them gracefully" heading in the article linked below. Note that this article is old and so is the package it uses. I.e. don't use the mentioned github.com/pkg/errors package, use the standard library's errors package.

https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully

Standard lib errors package
https://pkg.go.dev/errors