So usually when we run a method that can both fail and return a value, we can encode our method return type as Either[SomeErrorType, ReturnType]
. But many times
I use Try[Unit]
for that case.
It encodes that the result of the method either succeeds or fails with some Exception
, which can be further processed.
T => Unit
Try
lifts errors to the application level, encoding in the signature that some error can be expected and allowing the application to handle it as a value. Option[T]
=> Option is only able to encode that the operation had a value or notEither[SomeErrorType, Unit]
=> Try
It's easier to work with using monadic constructions. I've used something like this to implement checks. (imaginary example)
for {
entity <- receiveEntity // Try[Entity]
_ <- isRelational(entity)
_ <- isComplete(entity)
_ <- isStable(entity)
} yield entity
where each check is of the form: Entity => Try[Unit]
This will return the entity
if all checks pass of the first error that failed the check.
One more option that hasn't been mentioned yet is Validated
from cats. All the options mentioned so far (Try
, Either
, Option
) are monads, while Validated
is an applicative functor. In practice this means you can accumulate errors from multiple methods returning Validated
, and you can do several validations in parallel. This might not be relevant to you, and this is a bit orthogonal to the original question, but I still feel it's worth mentioning in this context.
As for the original question, using Unit
return type for a side-effecting function is perfectly fine. The fact this function can also return error shouldn't get in your way when you define the "real" (right, successful, etc.) return type. Therefore, if I were to select from your original options, I'd go for Either[Error, Unit]
. It definitely doesn't look odd to me, and if anyone sees any drawbacks in it, I'd like to know them.