简体   繁体   中英

Are there problems using Try or Either for error handling in public/open source Scala APIs?

I notice that a lot of open-source Scala APIs use Future (like Slick) or Java-style exception handling (like spray-json) rather than Try or Either .

Are there problems returning Try or Either in simple, open-source APIs that aren't long-running or asynchronous? Should I avoid these and, if so, what should I use instead?

(Obviously, I'd prefer to use core Scala APIs rather than third-party libraries, such as Scalaz, to avoid forcing downstream users to use these libraries as well.)

Try is fine for wrapping exceptions for non asynchronous operations.
However, I consider that using an exception instance to return a non-exceptional error condition is not correct . I also hold this true when the API is asynchronous and using Future . Mixing exceptional errors with business errors easily leads to incorrect/insufficient error handling by the clients of the API (aka by me :) ).

If you decide to still use exceptions to denote your business errors, please remember that exceptions come with a hidden price: building the stack trace. This can be alleviated if you create your own business exceptions and mix scala.util.control.NoStackTrace .

The default Either implementation in the Scala standard library is unbiased which means that distinguishing between the success and failure cases relies only on a convention :

Right is right (correct), therefore Left is wrong (incorrect).

The unbiased side also makes it painful to follow the "happy" path (or to make error recovery stand out)

Your best options at this point (scala 2.11.6 and the corresponding standard library) will vary according to your dependencies and the API you want to expose to your users but it will not be from the scala standard library.

  • Scalaz Validation or \\/ are an option if you already depend heavily of scalaz
  • Scalactic's Or type is a nice lightweight solution if you don't want to pull in the whole scalaz. It is mature and was extracted from scalatest
  • (from the comments) Accord could be an option but I don't know enough about it to compare it with the others.
  • (from the comments) Cat's Xor could be an option but I don't know enough about it to compare it with the others.
  • Rapture modes can allow you to let the client of the API decide what type he wants to get back (this approach is used in parboiled2 for instance). I have no idea what the cost is for the library author or for the compilation times though.

You may also already be depending on a library which offers its own validation type. For instance, play-json has a JsResult type with validation like abilities. It may make sense to reuse a type depending on what you are working on.

There is a slip in progress and an expert group call to add a biased either type to the standard library in Scala. The above 2 solutions have the added benefit of already being able to accumulate errors.

Return a Try[T] when: there's a semantic difference between a correct result and incorrect result (that is, an error). This because:

  • Try is right-biased (it works in map, flatmap, and for-comprehensions).

  • The failure case HAS to be a Throwable, and this is important because a throwable will always clearly mean that something is wrong even beyond the boundary of your code.

Caveat : It's true that stack trace generation is an overhead sometimes, but not always! Think twice before prematurely optimizing your code: stack traces carry a lot of useful debug information. Should you later discover you really need to skip stack trace generation, no problem: use with NoStackTrace as shown above.

def validate(s:String):Try[String] = Option(s).filter(!_.isEmpty).map(Try(_)).getOrElse(new Exception("bad input") with NoStackTrace 

Use Either[L,R] when : both the cases are acceptable, but they are mutually exclusive (ie Either[Integer,Float]). For this reason, despite what Scalaz guys have to say about this, I believe it's OK that Either is unbiased.

def parseNum(s:String):Either[Int,String] = Try(parseInt(s)).map(Left(_)).getOrElse(Right(s))

Use a Tuple when similarly to Either, but when you have N, non mutually exclusive values (you know all will be useful)

def parseAndReturn(s:String):(Integer,String) = (parseInt(s), s) 

Return an Option[T] when the T can be null, or when you're in the same situation as in a Try[T], but you don't give as***t about why stuff went wrong.

def attemptToparse(s:String):Option[Result] = Try(parse(s)).toOption

I agree that Try is for capture/reifying exceptions, not for returning business/validation errors. Scalaz is a very good alternative, but if you don't want to depend on it, I think Either is ok, being unbiased is annoying but not a deal breaker IMHO: Once you establish the convention that Right is the valid result, you can "right bias" it with .rightProjection (and if you want to deal only with the errors, you can do a left projection. Another option is to use Either's fold fold[X](fa: (A) ⇒ X, fb: (B) ⇒ X): X like myEither.fold(dealWithErrors _, happyPath _ ) (although it means both dealWithErrors and happyPath returns the same type)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM