16

Not too long ago I started using Scala instead of Java. Part of the "conversion" process between the languages for me was learning to use Eithers instead of (checked) Exceptions. I've been coding this way for a while, but recently I started wondering if that's really a better way to go.

One major advantage Either has over Exception is better performance; an Exception needs to build a stack-trace and is being thrown. As far as I understand, though, throwing the Exception isn't the demanding part, but building the stack-trace is.

But then, one can always construct/inherit Exceptions with scala.util.control.NoStackTrace, and even more so, I see plenty of cases where the left side of an Either is in fact an Exception (forgoing the performance boost).

One other advantage Either has is compiler-safety; the Scala compiler won't complain about non-handled Exceptions (unlike the Java's compiler). But if I'm not mistaken, this decision is reasoned with the same reasoning that is being discussed in this topic, so...

In terms of syntax, I feel like Exception-style is way clearer. Examine the following code blocks (both achieving the same functionality):

Either style:

def compute(): Either[String, Int] = {

    val aEither: Either[String, String] = if (someCondition) Right("good") else Left("bad")

    val bEithers: Iterable[Either[String, Int]] = someSeq.map {
        item => if (someCondition(item)) Right(item.toInt) else Left("bad")
    }

    for {
        a <- aEither.right
        bs <- reduce(bEithers).right
        ignore <- validate(bs).right
    } yield compute(a, bs)
}

def reduce[A,B](eithers: Iterable[Either[A,B]]): Either[A, Iterable[B]] = ??? // utility code

def validate(bs: Iterable[Int]): Either[String, Unit] = if (bs.sum > 22) Left("bad") else Right()

def compute(a: String, bs: Iterable[Int]): Int = ???

Exception style:

@throws(classOf[ComputationException])
def compute(): Int = {

    val a = if (someCondition) "good" else throw new ComputationException("bad")
    val bs = someSeq.map {
        item => if (someCondition(item)) item.toInt else throw new ComputationException("bad")
    }

    if (bs.sum > 22) throw new ComputationException("bad")

    compute(a, bs)
}

def compute(a: String, bs: Iterable[Int]): Int = ???

The latter looks a lot cleaner to me, and the code handling the failure (either pattern-matching on Either or try-catch) is pretty clear in both cases.

So my question is - why use Either over (checked) Exception?

Update

After reading the answers, I realized that I might have failed to present the core of my dilemma. My concern is not with the lack of the try-catch; one can either "catch" an Exception with Try, or use the catch to wrap the exception with Left.

My main problem with Either/Try comes when I write code that might fail at many points along the way; in these scenarios, when encountering a failure, I have to propagate that failure throughout my entire code, thus making the code way more cumbersome (as shown in the aforementioned examples).

There is actually another way of breaking the code without Exceptions by using return (which in fact is another "taboo" in Scala). The code would be still clearer than the Either approach, and while being a bit less clean than the Exception style, there would be no fear of non-caught Exceptions.

def compute(): Either[String, Int] = {

  val a = if (someCondition) "good" else return Left("bad")

  val bs: Iterable[Int] = someSeq.map {
    item => if (someCondition(item)) item.toInt else return Left("bad")
  }

  if (bs.sum > 22) return Left("bad")

  val c = computeC(bs).rightOrReturn(return _)

  Right(computeAll(a, bs, c))
}

def computeC(bs: Iterable[Int]): Either[String, Int] = ???

def computeAll(a: String, bs: Iterable[Int], c: Int): Int = ???

implicit class ConvertEither[L, R](either: Either[L, R]) {

  def rightOrReturn(f: (Left[L, R]) => R): R = either match {
    case Right(r) => r
    case Left(l) => f(Left(l))
  }
}

Basically the return Left replaces throw new Exception, and the implicit method on either, rightOrReturn, is a supplement for the automatic exception propagation up the stack.

  • Read this: mauricio.github.io/2014/02/17/… – Robert Harvey Sep 8 '16 at 16:24
  • @RobertHarvey Seems like most of the article discusses Try. The part about Either vs Exception merely states that Eithers should be used when the other case of the method is "non-exceptional". First, this is a very, very vague definition imho. Second, is it really worth the syntax penalty? I mean, I really wouldn't mind using Eithers if it weren't for the syntax overhead they present. – Eyal Roth Sep 8 '16 at 16:42
  • Either looks like a monad to me. Use it when you need the functional composition benefits that monads provide. Or, maybe not. – Robert Harvey Sep 8 '16 at 16:55
  • 2
    @RobertHarvey: Technically speaking, Either by itself is not a monad. The projection to either the left side or the right side is a monad, but Either by itself isn't. You can make it a monad, by "biasing" it to either the left side or the right side, though. However, then you impart a certain semantic on the both sides of an Either. Scala's Either was originally unbiased, but was biased rather recently, so that nowadays, it is in fact a monad, but the "monadness" is not an inherent property of Either but rather a result of it being biased. – Jörg W Mittag Sep 8 '16 at 17:12
  • @JörgWMittag: Well, good. That means that you can use it for all of its monadic goodness, and not be particularly bothered by how "clean" the resulting code is (though I suspect that functional-style continuation code would actually be cleaner than the Exception version). – Robert Harvey Sep 8 '16 at 17:14
13

If you only use an Either exactly like an imperative try-catch block, of course it's going to look like different syntax to do exactly the same thing. Eithers are a value. They don't have the same limitations. You can:

  • Stop your habit of keeping your try blocks small and contiguous. The "catch" part of Eithers doesn't need to be right next to the "try" part. It may even be shared in a common function. This makes it much easier to separate concerns properly.
  • Store Eithers in data structures. You can loop through them and consolidate error messages, find the first one that didn't fail, lazily evaluate them, or similar.
  • Extend and customize them. Don't like some boilerplate you're forced to write with Eithers? You can write helper functions to make them easier to work with for your particular use cases. Unlike try-catch, you are not permanently stuck with only the default interface the language designers came up with.

Note for most use cases, a Try has a simpler interface, has most of the above benefits, and usually meets your needs just as well. An Option is even simpler if all you care about is success or failure and don't need any state information like error messages about the failure case.

In my experience, Eithers aren't really preferred over Trys unless the Left is something other than an Exception, perhaps a validation error message, and you want to aggregate the Left values. For example, you are processing some input and you want to collect all the error messages, not just error out on the first one. Those situations don't come up very often in practice, at least for me.

Look beyond the try-catch model and you'll find Eithers much more pleasant to work with.

Update: this question is still getting attention, and I hadn't seen the OP's update clarifying the syntax question, so I thought I'd add more.

As an example of breaking out of the exception mindset, this is how I would typically write your example code:

def compute(): Either[String, Int] = {
  Right(someSeq)
    .filterOrElse(someACondition,            "someACondition failed")
    .filterOrElse(_ forall someSeqCondition, "someSeqCondition failed")
    .map(_ map {_.toInt})
    .filterOrElse(_.sum <= 22, "Sum is greater than 22")
    .map(compute("good",_))
}

Here you can see that the semantics of filterOrElse perfectly capture what we are primarily doing in this function: checking conditions and associating error messages with the given failures. Since this is a very common use case, filterOrElse is in the standard library, but if it wasn't, you could create it.

This is the point where someone comes up with a counter-example that can't be solved precisely the same way as mine, but the point I'm trying to make is there is not just one way to use Eithers, unlike exceptions. There are ways to simplify the code for most any error-handling situation, if you explore all the functions available to use with Eithers and are open to experimentation.

  • 1. Being able to separate the try and catch is a fair argument, but couldn't this be easily achieved with Try? 2. Storing Eithers in data structures could be done alongside the throws mechanism; isn't it simply an extra layer on top of handling failures? 3. You can definitely extend Exceptions. Sure, you can't change the try-catch structure, but handling failures is so common that I definitely want the language to give me a boilerplate for that (which I'm not forced to use). That's like saying that for-comprehension is bad because it's a boilerplate for monadic operations. – Eyal Roth Sep 8 '16 at 18:05
  • I just realized that you said that Eithers can be extended, where clearly they cannot (being a sealed trait with only two implementations, both final). Can you elaborate on that? I'm assuming you didn't mean the literal meaning of extending. – Eyal Roth Sep 9 '16 at 2:30
  • 1
    I meant extended as in the English word, not the programming keyword. Adding functionality by creating functions to handle common patterns. – Karl Bielefeldt Sep 9 '16 at 3:54
8

The two are actually very different. Either's semantics are much more general than just to represent potentially failing computations.

Either allows you to return either one of two different types. That's it.

Now, one of those two types may or may not represent an error and the other may or may not represent success, but that is only one possible use case of many. Either is much, much more general than that. You could just as well have a method that reads input from the user who is allowed to either enter a name or a date return an Either[String, Date].

Or, think about lambda literals in C♯: they either evaluate to an Func or an Expression, i.e. they either evaluate to an anonymous function or they evaluate to an AST. In C♯, this happens "magically", but if you wanted to give a proper type to it, it would be Either<Func<T1, T2, …, R>, Expression<T1, T2, …, R>>. However, neither of the two options is an error, and neither of the two is either "normal" or "exceptional". They are just two different types that may be returned from the same function (or in this case, ascribed to the same literal). [Note: actually, Func has to be split into another two cases, because C♯ doesn't have a Unit type, and so something that doesn't return anything cannot be represented the same way as something that returns something, so the actual proper type would be more like Either<Either<Func<T1, T2, …, R>, Action<T1, T2, …>>, Expression<T1, T2, …, R>>.]

Now, Either can be used to represent a success type and a failure type. And if you agree on a consistent ordering of the two types, you can even bias Either to prefer one of the two types, thus making it possible to propagate failures through monadic chaining. But in that case, you are imparting a certain semantic onto Either that it does not necessarily possess on its own.

An Either that has one of its two types fixed to be an error type and is biased to one side to propagate errors, is usually called an Error monad, and would be a better choice to represent a potentially failing computation. In Scala, this kind of type is called Try.

It makes much more sense to compare the use of exceptions with Try than with Either.

So, this is reason #1 why Either might be used over checked exceptions: Either is much more general than exceptions. It can be used in cases where exceptions don't even apply.

However, you were most likely asking about the restricted case where you just use Either (or more likely Try) as a replacement for exceptions. In that case, there are still some advantages, or rather different approaches possible.

The main advantage is that you can defer handling the error. You just store the return value, without even looking at whether it is a success or an error. (Handle it later.) Or pass it somewhere else. (Let someone else worry about it.) Collect them in a list. (Handle all of them at once.) You can use monadic chaining to propagate them along a processing pipeline.

The semantics of exceptions are hardwired into the language. In Scala, exceptions unwind the stack, for example. If you let them bubble up to an exception handler, then the handler cannot get back down where it happened and fix it. OTOH, if you catch it lower in the stack before unwinding it and destroying the necessary context to fix it, then you might be in a component that is too low-level to make an informed decision about how to proceed.

This is different in Smalltalk, for example, where exceptions don't unwind the stack and are resumable, and you can catch the exception high up in the stack (possibly as high up as the debugger), fix up the error waaaaayyyyy down in the stack and resume the execution from there. Or CommonLisp conditions where raising, catching and handling are three different things instead of two (raising and catching-handling) as with exceptions.

Using an actual error return type instead of an exception allows you to do similar things, and more, without having to modify the language.

And then there's the obvious elephant in the room: exceptions are a side-effect. Side-effects are evil. Note: I'm not saying that this is necessarily true. But it is a viewpoint some people have, and in that case, the advantage of an error type over exceptions is obvious. So, if you want to do functional programming, you might use error types for the sole reason that they are the functional approach. (Note that Haskell has exceptions. Note also that nobody likes to use them.)

  • Love the answer, though I still don't see why should I forgo Exceptions. Either seems like a great tool, but yes, I'm specifically asking about handling failures, and I'd rather use a much more specific tool for my task than an all-powerful one. Deferring failure handling is a fair argument, but one can simply use Try on a method throwing an Exception if he wishes not to handle it at the moment. Doesn't stack-trace unwinding affect Either/Try as well? You may repeat the same mistakes when propagating them incorrectly; knowing when to handle a failure is not trivial in both cases. – Eyal Roth Sep 8 '16 at 18:36
  • And I can't really argue with "purely functional" reasoning, can't I? – Eyal Roth Sep 8 '16 at 18:36
0

The nice thing about exceptions is that the originating code has already classified the exceptional circumstance for you. The difference between an IllegalStateException and a BadParameterException is much easier to differentiate in stronger typed languages. If you try to parse "good" and "bad", you aren't taking advantage of the extremely powerful tool at your disposal, namely the Scala compiler. Your own extensions to Throwable can include as much information as you need, in addition to the textual message string.

This is the true utility of Try in the Scala environment, with Throwable acting as the wrapper of left items to make life easier.

  • 2
    I do not understand your point. – Eyal Roth Sep 8 '16 at 18:43
  • Either has style issues because it is not a true monad (?); the arbitrariness of the left value steers away from the higher value added when you properly enclose information about exceptional conditions in a proper structure. So comparing the use of Either in one case returning strings in the left side, versus try/catch in the other case using properly typed Throwables is not proper. Because of the value of Throwables for providing information, and the concise manner that Try handles Throwables, Try is a better bet than either ... Either or try/catch – BobDalgleish Sep 8 '16 at 21:54
  • I'm actually not concerned about the try-catch. As said in the question the latter looks a lot cleaner to me, and the code handling the failure (either pattern-matching on Either or try-catch) is pretty clear in both cases. – Eyal Roth Sep 9 '16 at 2:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.