How to handle errors on JVM faster

There are various ways to handle errors in programming languages: 3r33371.
standard exceptions for many languages ​​(Java, Scala and other JVM, python, and many others) 3r3105.  
status codes or flags (Go, bash)
various algebraic data structures, the values ​​of which can be both successful results and error descriptions (Scala, haskell, and other functional languages) 3r3105.  
Exceptions are used very widely, on the other hand they are often said to be slow. But opponents of the functional approach often appeal to performance. 3r33333.
Recently, I have been working with Scala, where I can equally use both exceptions and different types of data for error handling, so I wonder which approach will be more convenient and faster. 3r33333.
Immediately discard the use of codes and flags, since this approach is not accepted in the JVM languages ​​and in my opinion too prone to errors (I apologize for the pun). Therefore, we will compare exceptions and different types of ATD. In addition, ADT can be considered as the use of error codes in a functional style. 3r33333.
3r3111. UPDATE 3r3–3112. : added exceptions without stack traces to 3x3r3371. 3r3338.
3r3342. Contestants
A little more about algebraic data types [/b]
For those who are not very familiar with ADT (3r3351. ADT 3r33285.) - the algebraic type consists of several possible values, each of which can be a composite value (structure, record). 3r33333.
An example is the type Option[T]= Some (value: T) | None , which is used instead of null: value of this type can be either Some (t) if the value is, or None if it is not. 3r33333.

Another example could be Try[T]= Success (value: T) | Failure (exception: Throwable) , which describes the result of the calculation, which could be completed successfully or with an error. 3r33333.

So our contestants:

  • The old Good 3r3328. exceptions  
  • There are exceptions without a stack trace, since it is the filling of the stack trace that is a very slow operation  
  • Try[T]= Success (value: T) | Failure (exception: Throwable) - the same exceptions, but in a functional wrapper  
  • Either[String, T]= Left (error: String) | Right (value: T) - A type containing either a result or a description of the error  
  • ValidatedNec[String, T]= Valid (value: T) | Invalid (errors: List[String]) - type of Cats libraries. which, in the event of an error, may contain several messages about different errors (it does not use exactly List , but this is not important)  


3r3111. NOTE 3r3-33112. in fact, exceptions are compared with stack-based, without and ATD, but several types are chosen, since Scala does not have a unified approach and it is interesting to compare several. 3r33333.

In addition to exceptions, strings are used here to describe errors, but with the same success, different classes would be used in a real situation ( Either[Failure, T]3rr3370.). 3r33333.
3r3122. The problem is

For testing error handling, let's take the problem of parsing and validating dаta: 3r-3371.

    case class Person (name: String, age: Int, isMale: Boolean)
type Result[T]= Either[String, T]
trait PersonParser {
def parse (dаta: Map[String, String]): Result[Person]

those. having raw data Map[String, String] need to get Person or an error if the data is not valid. 3r33333.
3r33150. Throw


head on with the use of exceptions (hereinafter I will only give the function 3r36969. person , the full code can be found at 3r3-3159. github ):
ThrowParser.scala 3r33333.
    def person (dаta: Map[String, String]): Person = {3r3333378. val name = string (data.getOrElse ("name", null))
val age = integer (data.getOrElse ("age", null))
val isMale = boolean (data.getOrElse ("isMale", null))
require (name.nonEmpty, "name should not be empty")
require (age> ? "age should be positive")
Person (name, age, isMale)

here 3r33369. string , integer and boolean validates the presence and format of simple types and performs the conversion.
In general, quite simple and understandable. 3r33333.
3r3193. ThrowNST (No Stack Trace)

The code is the same as in the previous case, but exceptions are used without the stack trace where it is possible: ThrowNSTParser.scala 3r33333.



The solution catches exceptions earlier and allows you to combine the results through 3r33369. for (not to be confused with cycles in other languages):
TryParser.scala 3r33333.

    def person (dаta: Map[String, String]): Try[Person]= for {
name <- required(data.get("name"))
age <- required(data.get("age")) flatMap integer
isMale <- required(data.get("isMale")) flatMap boolean
_ <- require(name.nonEmpty, "name should not be empty")
_ <- require(age > ? "age should be positive")
} yield Person (name, age, isMale)

a bit more unusual for a weak eye, but at the expense of using for In general, it is very similar to the version with exceptions, besides, the validation of the presence of the field and the parsing of the desired type occur separately ( flatMap here you can read as 3r33333. and then ) 3r3333371.



There is a type Either hidden behind the alias Result since the type of error is fixed:
EitherParser.scala 3r33333.

    def person (dаta: Map[String, String]): Result[Person]= for {
name <- required(data.get("name"))
age <- required(data.get("age")) flatMap integer
isMale <- required(data.get("isMale")) flatMap boolean
_ <- require(name.nonEmpty, "name should not be empty")
_ <- require(age > ? "age should be positive")
} yield Person (name, age, isMale)

Since the standard Either as Try forms a monad in Scala, then the code came out exactly the same, the difference here is that the error appears here as a string and the exceptions are used minimally (only for error handling when parsing the number) 3r3371.



Here the Cats library is used in order to get in the event of an error not the first thing that happened, but as much as possible (for example, if several fields were not valid, then the result will contain errors of parsing all these fields) 3r3333366.  
ValidatedParser.scala 3r33333.

    def person (dаta: Map[String, String]): Validated[Person]= {
val name: Validated[String]=
required (data.get ("name")) 3r33333. .ensure (one ("name should not be empty")) (_. nonEmpty)
val age: Validated[Int]=
required (data.get ("age")) 3r3333378. .andThen (integer)
.ensure (one ("age should be positive")) (_> 0)
val isMale: Validated[Boolean]=
required (data.get ("isMale"))
.andThen (boolean)
(name, age, isMale) .mapN (Person)

This code is less similar to the original version with exceptions, but checking for additional restrictions is not divorced from parsing fields, and we still get a few errors instead of one, it's worth it! 3r33333.
3r33333. Testing

For testing, a set of data was generated with a different percentage of errors and parsed in each of the methods. 3r33333.

Result on all percent of errors:
How to handle errors on JVM faster  

In more detail on a low percentage of errors (time is different here since it was used 3-???. About 3r3328. A sample): 3r-3366.  

If any part of the errors is an exception with the stack trace (in our case, the error of parsing the number will be an exception that we do not control), then of course the performance of the “fast” error handling methods will significantly deteriorate. Especially suffering Validated , since it collects all errors and as a result receives a slow exception more than others:
3r33333. 3r33333.



As the experiment showed, exceptions with stack-traces are really very slow (100% error is the difference between 3r3693? Throw

? and 3r3333369. Either More than 50 times!), And when there are almost no exceptions, the use of ADT has its price. However, using exceptions without stack traces is as fast (and with a low percentage of errors faster) as ADT, but if such exceptions go beyond the same validation, it will not be easy to track their source. 3r33333.

So, if the probability of an exception is more than 1%, exceptions without stack traces work the fastest, Validated or the usual Either almost as fast. With a large number of errors Either may be slightly faster Validated only due to fail-fast semantics. 3r33333.

The use of ADT for error handling gives another advantage over exceptions: the possibility of an error is sewn into the type itself and is harder to miss, as with the use of 3r36969. Option instead of nulls. 3r33333.

+ 0 -

Add comment