Let's say I have an app with three layers: a view (JSON endpoint), service, 
and persistence layer.


Now a `NotFoundError` error occurs at the persistence layer when a record 
cannot be found. `NotFoundError` is a simple wrapper around a lower level 
database driver error that I don't want the application to be aware of 
(since it's an implementation detail).


For example, if the `gocql` driver emits an `ErrNotFound` error, the 
persistence layer will wrap it in a custom error type so that the original 
error is preserved but not exposed.


package database

type NotFoundError struct {
  originalErr error // persistence layer wraps the driver error
}



The service layer then type switches on whatever errors it receives from 
lower layers. For example in a call to the persistence layer:



package service

func (s *Service) GetSomething(id string) (string, error) {
  s, err := database.Get(id)
  if err != nil {
    return "", handleError(err, “service: failed to get something”)
  }
}


func handleError(err error, context, message string) error {
  switch err.(type) {
    case database.NotFoundError:
      return &ServiceError{
        Code: NotFoundCode,
        Message: message,
        originalErr: errors.Wrap(err, context),
      }
    default:
     ...
  }
}



a service error is a custom error type that looks like:


type ServiceError struct {
 originalErr error
 Code    int             `json:"code"`
 Field string            `json:"target,omitempty"`
 Message string          `json:"message,omitempty"`
 Details []*ServiceError `json:"details,omitempty"`
}




To recap, the error propagates through the following layers:


driver -> persistence -> service -> view


Each layer type switches on the error type it receives from the preceding 
layer. 

Errors may be recursively nested (the "Details" field is a []*ServiceError) 
and "originalErr" contains the original error with a stack trace provided 
by the pkg/errors <https://github.com/pkg/errors> library.


How would one approach unit testing something like this? 


Doing a simple reflect.DeepEqual on the actual and expected error values 
would be ideal, but the stack trace contained in the actual error means 
that the expected error will always fail the equality comparison (since 
DeepEqual also parses unexported fields). In addition, the order of errors 
in the `Details` slice is also unreliable (for example when validating 
fields, the order shouldn't matter) which further complicates trying to 
compare things. I’d have to pollute my tests with loops, manual comparisons 
and recursive logic which will itself be error-prone.


I’ve looked through some popular projects on GitHub and I can’t find any 
examples of code similar to this this which leads me to believe that all 
these abstraction hierarchies and complex errors types are horribly 
unidiomatic Go... I now know why people say Go is more of a systems 
language. But anyway... is there a better way to deal with errors in an app 
structured like this? How would you go about comparing these types of error 
values? How do you handle rich errors types with lots of contextual 
information in your own web applications?

-- 
You received this message because you are subscribed to the Google Groups 
"golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to golang-nuts+unsubscr...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to