Skip to main content

Either & Ior

Both Either<E, A> and Ior<E, A> hold values that may be of type E or A. By convention, the type E represents errors and the type A represents success. For example, Either<DbError, User> could be a good result type for a function that accesses a database and returns a User but may also fail with a DbError. Another point of view is that both types extend the capabilities of the built-in Result type, but no longer constraining the potential errors to be Throwable.

Either<E, A> only admits these two possibilities: a Left holding a value of type E or a Right holding a value of type A. On the other hand, Ior<E, A> provides a third option, namely Both. Using Both, you can represent states that are considered successful but with some potential errors during execution; like a compiler that finishes successfully but has some warnings. Nevertheless, Ior is not used very often.

Using builders

The preferred way to work with Either and Ior is to use builders. Those start with a call to either or ior followed by a lambda; inside that block, we can access the uniform typed errors API with functions like raise, ensure, and recover.

import arrow.core.raise.either
import arrow.core.raise.ensure

data class MyError(val message: String)

fun isPositive(i: Int): Either<MyError, Int> = either {
ensure(i > 0) { MyError("$i is not positive") }
i
}

suspend fun example() {
isPositive(-1) shouldBe MyError("-1 is not positive").left()
isPositive(1) shouldBe 1.right()
}

To give you the complete picture, inside those blocks, the potential errors are represented by a receiver of type Raise<E>. Functions with that receiver can be transformed into a variety of types; not only Either and Ior, but also Result, Option, or a nullable type.

A common scenario is to have an Either or Ior value that we want to execute as part of the block. That is, we want potential errors in those values to bubble as errors of the entire block or keep the execution if the value represents success. In those cases, we need to call .bind() over the value of type Either or Ior.

tip

We recommend using the custom NoEffectScopeBindableValueAsStatement rule for Detekt to prevent forgetting .bind() inside an either or ior block.

Combining Ior errors

The flow in an Either block is simple: we execute each step; if at some point we bind() a Left or find a raise, we stop and return that value; if we get to the end, we wrap the result in Right. ior blocks are a bit more complicated, since we may end up in a situation in which we have errors to be reported, yet we also have a value to continue the execution. This brings up a question: what should we do if several steps in the block are Both? The current API leaves the answer open to the developer. The ior builder has an additional parameter that specifies how to combine several errors.

Without builders

In some scenarios, builders may be overkill for the task at hand. For those cases, we provide functions that create or operate directly on Either and Ior.

On the generation front, extension functions like .left() and .right() provide another way to write expressions that won't obscure the inner contents as much as a constructor. Validations are often written in that style.

// this is the type we want to construct
@JvmInline value class Age(val age: Int)

// these are the potential problems
sealed interface AgeProblem {
object InvalidAge: AgeProblem
object NotLegalAdult: AgeProblem
}

// validation returns either problems or the constructed value
fun validAdult(age: Int): Either<AgeProblem, Age> = when {
age < 0 -> AgeProblem.InvalidAge.left()
age < 18 -> AgeProblem.NotLegalAdult.left()
else -> Age(age).right()
}

Another way to obtain an Either is using Either.catch, which wraps a computation that may throw exceptions and returns a Left if that's the case. Essentially, runCatching from the standard library, but replacing Result with Either.

The rest of the API closely follows the one from typed errors. For example, you can call recover or zipOrAccumulate directly on Either without the need for an additional either { } block. One potentially useful function not part of builders is mapLeft, which applies a function when the value represents an error. This scenario often arises when your code has a hierarchy of different error types.

Either for validation

Either has a double face: it can be used to model problems in a piece of code, pretty much like exceptions, but also to define validations over some input data. The difference between these two scenarios is how we react to several problems arising in a piece of code.

  • When we think of exceptions, we have a fail-fast or fail-first approach: Once we discover a problem, we want to stop execution and immediately report to the caller. In those scenarios, steps depend on one another, so it makes no sense to keep trying.
  • When we think of validation, we want to be as comprehensive as possible with potential problems with the input data. In other words, if the given name and age are wrong, we want to report both, not just the first one. This approach is called accumulation and arises when the code is computations whose failure is independent of each other.

By default, an either block follows the first approach. If you want to accumulate errors instead, you should use zipOrAccumulate, or mapOrAccumulate. The difference is that the former takes the different computations as arguments, and they can return different types, whereas the latter applies the same computation uniformly to elements of an Iterable.

One common pattern when describing validations is to have an Either with List<Problem> as the error type. Arrow provides a more refined version where we ensure that we never end up in an awkward situation in which we have a Left value, but the list of problems is empty.

public typealias EitherNel<E, A> = Either<NonEmptyList<E>, A>

In Arrow 1.x series, a different type called Validation embodied the accumulation strategy for errors. However, the API was almost identical, and sometimes code became flooded with conversion back and forth between Either and Validation. Arrow 2.x provides a single Either type instead, but we encourage you to use the EitherNel type alias if you are describing a validation.