modernice / goes

goes is an event-sourcing framework for Go.
https://goes.modernice.dev
Apache License 2.0
134 stars 12 forks source link

Enriched command errors #111

Closed bounoable closed 1 year ago

bounoable commented 1 year ago

Problem

Currently, the command bus has limited capabilities to handle errors returned by the command handler. Specifically, it can only retain the error message, which makes it hard to handle the error from the calling side. There's also no support for error codes, which are the typically the main value to check for when handling errors.

Requirements

The command handler must be able to add additional data to an error:

Proposal (gRPC error details)

gRPC error details

Proposal is to add support for Protocol Buffers, similar to how Connect does. An error that provides a Details() []proto.Message method can then enrich the error with arbitrary data.

For convenience, we can provide an *Error type that provides the most essential features out of the box.

Command handler

package example

func example() {
  var underlyingError error // the actual error that made the command fail
  var code int // the custom error code
  var details []proto.Message

  err := command.NewError(code, underlyingError, command.WithErrorDetail(details...)) // *command.Err

  // err.Code() == code
  // err.Error() == err.Error()

  for i, d := range err.Details() {
    // d == *command.ErrorDetail{...}
    v, err := d.Value()
    if err != nil {
      panic(fmt.Errorf("failed to unmarshal error detail: %w", err))
    }

    // v == details[i] // not actually the same instance but a copy
  }
}

Command dispatcher

package example

func example(bus command.Bus, cmd command.Command) {
  err := bus.Dispatch(context.TODO(), cmd, dispatch.Sync())

  cerr := command.Error(err) // parse the error

  // cerr == *command.Err{...}
  // cerr.Code() == int(...)
  // cerr.Message() == "..."

  // Either manually iterate the details
  for i, d := range cerr.Details() {
    v, err := d.Value()
    if err != nil {
      panic(fmt.Errorf("failed to unmarshal error detail: %w", err))
    }

    switch v := v.(type) {
    case *errdetails.LocalizedMessage:
      log.Println(v.GetLocale(), v.GetMessage())
    }
  }

  // Or use provided methods on the *Error type
  msg, ok := cerr.LocalizedMessage("en-US")
  // msg == "..."
}
bounoable commented 1 year ago

Implemented in 67438ecd03da64b31e8167d8d53c2b02c0fdc1ba.