modernice/goes

Enriched command errors

Closed this issue · 1 comments

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:

  • Error code
  • Human-readable, localized error messages
  • Debug information for development (?)

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 == "..."
}

Implemented in 67438ec.