When using the left side of `Either` for failures,...
# arrow
c
When using the left side of
Either
for failures, what do kind of data do you store? So far, I'm working on a fairly small project where I just store the kind of error (authentication, validation…) and a String message, but I feel like that won't scale as a more general solution. Any guidelines/recommendations?
c
I think most people would try to enumerate their domain errors. You might have a sealed class of
AuthenticationErrors
which describe the errors and store w/e information you deem necessary for a later inspection.
(fwiw, I am a Haskeller and not writing production Kotlin so maybe the advice is different)
c
That sounds very tedious, and I'm not convinced it's worth it 🤔 95% of the time the error is just displayed to the user. Also, that means having serialization support for each failure, and I'd rather avoid tainting the domain module with serialization
c
I would probably just have a
toHuman
like function that produces a human readable version of the error.
c
Would you send that complex object to the client, or just the human representation?
c
Just the human representation, the object would go into a log for devs.
j
you want to have an enum in your server, that your controller translates into something for your client
c
It depends on how the client handles errors too.
c
Currently, the client just displays the error message to the user. I don't have i18n in this app anyway.
I guess enums is better for i18n
m
@CLOVIS We use arrow for some large critical projects in production. The left channel are normally sealed classes, which allowed us explicitly type and handle domain failures in various layers all the way to the controller. For instance, we can make a http client explicitly propagate a
Ratelimited(val retryAfter: Duration) : PatchUserFailure()
left with a particular retriable after information. This can be consumed by the caller to make informed decisions on how might it retry the call. We don't usually do this everywhere but this is useful for some heavy processes or important / sensitive microservices that are easily overloaded, where we can decide to defer the process to be either retried or to be eventually consistent using rate controlled queues. Another example would be a service layer returning
data class UserAlreadyExists(existingUser: User) : CreateUserFailure()
left when trying to create a user.
Regarding https://kotlinlang.slack.com/archives/C5UPMM0A0/p1672159871438649?thread_ts=1672158666.679269&cid=C5UPMM0A0 in our projects i believe only those left types produces by controller handler functions or middlewares are serializable. We generally declare a new type for those that are serializable outwards. This is so that we can be precise as for those fields that we should/should not serialize. E.g if a user has encrypted password hash, an attempt to serialize it would yield a 500 with nothing serialised, an immediate error with a special message in the logs about a sensitive type attempted to be serialised and a high priority page sent to oncall. This means most of the lefts produced by service classes, clients, stores, etc are not serializable. Those that needs to be serialised outwards normally need to be projected to a different type that is serializable in the caller's side. I.e. there's a transformation
(DomainFailure) -> FailureResponse
Some have
Loggable<T>
typeclass implemented separately and called via extension function on the class
fun DomainFailure.loggableJson(): Loggable<T>
to help transform them into a privacy safe loggable json as appropriate.
This may sound tedious but it's actually not as they quickly become easily adoptable patterns which are consistent across the codebase. With large projects and large teams, these practice easily scale and help the code organize and document itself.
g
I can confirm, it’s very tedious, we tried to work for a while, but ditched it Maybe we do not have the best approaches to work with this style, but for our very-ui oriented logic it was tedious with a lot of boilerplate to wrap and unwrap Do not discourage anyone, just want to add those 2 cents, that it will not be an easy ride
m
I see that's a good point. I guess it depends on how critical types are for the project. We manage a number of backend applications which are governing a suite of rather important and intricate functionalities. Any logic mistake or several seconds of downtime can easily yield a high severity page. We rely heavily on types especially on left either channels to assert correctness through compiler assistance. Such requirement combined with a large team, we need to get help from the compiler in order for the project to scale and for all of us to be able to sleep at night 😅
And therefore, Arrow is godsend for us. Either and Option plus sealed classes and the
either { }
and
option { }
builders are absolutely indispensable.
c
To clarify, I'm trying to create a 'common left type' that I can reuse everywhere to ensure good practices. Currently, it stores an "error kind", which is an enum representing in very broad strokes the category of the error (used for example to convert to HTTP status codes), as well as an error string. I'm wondering whether it's worth adding more information to that.
I agree that
either {}
makes everything much, much easier to understand
m
Ah right! I think we do have a project that employs something like that. It's a bit of a redefinition of the IO type so to be able to immediately short circuit a computation safely with a particular error response which are directly translatable to http responses with appropriate status codes,
data class Middleware<T>(val
`execute`:
suspend ExecutionScope.() -> Either<ErrorResponse, T>)
So we'll have something like
Copy code
class AuthorizationMiddlewares(...) {
  fun authorizeToken(...): Middleware<Either<AuthzFailure, AuthorizedUser>> = middleware {
    either {
      // etc
    }
  }
}
ErrorResponse is just a data class with a json body, an enum status code, an optional list of headers (i.e. for set-cookie headers)