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 Either
s instead of (checked) Exception
s. 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 Exception
s 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 Exception
s (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 Exception
s 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 Exception
s.
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.
6 Answers 6
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
andcatch
is a fair argument, but couldn't this be easily achieved withTry
? 2. StoringEither
s in data structures could be done alongside thethrows
mechanism; isn't it simply an extra layer on top of handling failures? 3. You can definitely extendException
s. Sure, you can't change thetry-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– Eyal Roth2016年09月08日 18:05:46 +00:00Commented Sep 8, 2016 at 18:05 -
I just realized that you said that
Either
s can be extended, where clearly they cannot (being asealed trait
with only two implementations, bothfinal
). Can you elaborate on that? I'm assuming you didn't mean the literal meaning of extending.Eyal Roth– Eyal Roth2016年09月09日 02:30:33 +00:00Commented Sep 9, 2016 at 2:30 -
3I meant extended as in the English word, not the programming keyword. Adding functionality by creating functions to handle common patterns.Karl Bielefeldt– Karl Bielefeldt2016年09月09日 03:54:36 +00:00Commented Sep 9, 2016 at 3:54
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
Exception
s.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 useTry
on a method throwing anException
if he wishes not to handle it at the moment. Doesn't stack-trace unwinding affectEither
/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– Eyal Roth2016年09月08日 18:36:03 +00:00Commented Sep 8, 2016 at 18:36 -
And I can't really argue with "purely functional" reasoning, can't I?Eyal Roth– Eyal Roth2016年09月08日 18:36:24 +00:00Commented Sep 8, 2016 at 18:36
-
Why is Exception a side effect? Is not it just a part of implicit return type?Basilevs– Basilevs2024年10月31日 00:35:25 +00:00Commented Oct 31, 2024 at 0:35
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.
-
2I do not understand your point.Eyal Roth– Eyal Roth2016年09月08日 18:43:10 +00:00Commented Sep 8, 2016 at 18:43
-
Either
has style issues because it is not a true monad (?); the arbitrariness of theleft
value steers away from the higher value added when you properly enclose information about exceptional conditions in a proper structure. So comparing the use ofEither
in one case returning strings in the left side, versustry/catch
in the other case using properly typedThrowables
is not proper. Because of the value ofThrowables
for providing information, and the concise manner thatTry
handlesThrowables
,Try
is a better bet than either ...Either
ortry/catch
BobDalgleish– BobDalgleish2016年09月08日 21:54:54 +00:00Commented Sep 8, 2016 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– Eyal Roth2016年09月09日 02:37:36 +00:00Commented Sep 9, 2016 at 2:37 -
@BobDalgleish No generic class is a monad on it's own. A monad is the combination of the class, a bind function and a unit function.
Either
can be part of a monad twice, either the left or the right parameter being held constant.Caleth– Caleth2025年01月07日 10:06:53 +00:00Commented Jan 7 at 10:06
Exceptions are quite good due to their deep integration in the Java ecosystem, and they should be used whenever they will do the job. Here's my view on when that would be, compared to some of the alternatives.
The sweet spot for exceptions is single-threaded code where you don't want to handle the exception except at some top-level entry point such as an HTTP endpoint or a Main routine. In these circumstances, exceptions are very powerful for the developer:
- They include a stack trace that shows where the original problem occurred and also the chain of events that led to it happening. This is a massive productivity boost for debugging, and debugging is where developers spend a lot of our time.
- Exceptions require no additional syntax to propagate them through the call stack.
The biggest place Either is helpful is if you want to process the failure on purpose rather than letting it propagate. Be careful before assuming this is obviously what you want to do, because it's easy to accidentally remove some information from a stack trace that the developer would have liked to know. However, if you are sure you want to handle the condition and keep going, then Either can help you be sure that you are catching the failure of the thing you want. Exceptions are really bad for catching them halfway in the callstack, because you don't really know what, in the tree of code that you invoked, actually caused the problem that occurred.
Either is also helpful if you are using a threading framework that does not propagate exceptions across threads. This is more unusual than it may seem, however. I can't even name a specific example. It's a common expectation that if a framework can defer work to another thread, and the work throws an excetion, then the exception will be caught and sent back to the originating thread.
A massive downside of Either is that it can be hard to remember what the "left" and "right" options of it correspond to. Try fixes this problem by changing "left/right" to instead be "success/failure". Also, with Try, a failure is always indicate by an exception. With Either, the failure is allowed to be any type, with exceptions just being a common choice.
Adding to the other answers, I'd like to respond to this snippet:
One major advantage
Either
has overException
is better performance; anException
needs to build a stack-trace and is being thrown. As far as I understand, though, throwing theException
isn't the demanding part, but building the stack-trace is.
Cost of stack traces
The stack traces can be computed lazily, so that you don't pay their cost for exceptions that you catch and recover from. If you do end up logging your error, then they're a useful thing to have, so it's not like you're paying a price for nothing.
Funnily enough, you'll see Either
/Result
based systems (like Rust) where people are asking how they can get back the price and benefit of stacktraces, to answer: "Where did this Err
come from, and how did it get here?". There's many Rust libraries that do just that.
The actual cost
... is the time taken to unwind the stack, and the increased executable size from the stack unwinding metadata.
The performance trade-off between exceptions and Either
/Result
/error-codes is actually non-obvious.
The Result
-style error-handling pays a small-but-consistent price for every function call, because there needs to be a branch to check for success vs failure, and respond accordingly.
Exception-style error-handling pays a high price for every exception exception that's actually thrown, plus a small-but-consistent code size price for every place an exception can be thrown or caught.
Notably, exceptions have zero runtime cost when there is no actual exception thrown, which means that for frequently called but rarely-failing operations, exceptions are faster than results (which are stuck checking for an error every time, on the off chance there is one).
Khalil Estell details this really well in this CPPCon 2024 talk, C++ Exceptions for Smaller Firmware.
You use an exception if there is a "normal" path and an "exceptional" path. You use "either" if there are two normal paths.
Say you have an employee and ask for his name. It is probably highly unusual that they don’t have a name, so no name throws an Exception.
Now you ask for the employee’s spouse. It is absolutely normal that many employees don’t have spouses - too young to be married, bachelor, divorced, widowed etc. So since having no spouse is not exceptional I’d return an Either: Either a Person or nil.
Now different languages have different cultures so if given two reasonable choices (both exception and Either seem reasonable) you may use what is preferred by the culture of this language.
Explore related questions
See similar questions with these tags.
Try
. The part aboutEither
vsException
merely states thatEither
s 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 usingEither
s if it weren't for the syntax overhead they present.Either
looks like a monad to me. Use it when you need the functional composition benefits that monads provide. Or, maybe not.Either
by itself is not a monad. The projection to either the left side or the right side is a monad, butEither
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 anEither
. Scala'sEither
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 ofEither
but rather a result of it being biased.