A Hitchhiker's Guide to the Functional Exception Handling in Java

Grzegorz Piwowarek @pivovarit

@pivovarit

Software Engineer | Trainer

commiter @ AssertJ, Vavr

blogger @ 4Comprehension

senior editor @ Baeldung

Exceptions:

  • Invisible in the source code
  • Create unexpected exit points
  • Behave like GOTOs
  • Easy to lose when async

"Every time you call a function that can raise an exception and don’t catch it on the spot, you create opportunities for surprise bugs caused by functions that terminated abruptly, leaving data in an inconsistent state, or other code paths that you didn’t think about."

October 13, 2003 by Joel Spolsky

Exceptions in Java are abused

  • Modeling absence
  • Modeling known business cases
  • Modeling alternate paths
    /**
     * Fills in the execution stack trace. This method records within this
     * {@code Throwable} object information about the current state of
     * the stack frames for the current thread.
     *
     * ...
     */
    public synchronized Throwable fillInStackTrace() {
       ...
    }

...and are not cheap.

http://normanmaurer.me/blog/2013/11/09/The-hidden-performance-costs-of-instantiating-Throwables/​

-XX:-OmitStackTraceInFastThrow​

https://www.javaspecialists.eu/archive/Issue187.html

Java Exceptions Antipatterns

  • Log and throw
  • Declaring that your method throws java.lang.Exception
  • Declaring that your method throws a large variety of exceptions
  • Catching java.lang.Exception
  • Destructive wrapping (throwing away stacks)
  • Log and return null
  • Catch and ignore
  • Throw within finally
  • Multi-line log messages
  • Unsupported operation returning null
  • Ignoring InterruptedException
  • Relying on getCause()

http://www.rockstarprogrammer.org/post/2007/jun/09/java-exception-antipatterns/​

Error Handling in Go!

func foo(s string) (string, error) {
    // implementation
}

"Values can be programmed, and since errors are values, errors can be programmed. Errors are not like exceptions. There’s nothing special about them, whereas an unhandled exception can crash your program."



https://blog.golang.org/errors-are-values 

response, err := foo("Dumbledore dies.")
if err != nil {
    // ...
}
// happy path like nothing happened

Error handling in Go does not disrupt the flow of control.

panic keyword

http://scrumreferencecard.com/scrum-reference-card/​

Compile-time checks result in shorter feedback cycles

public class Person {
    private final String name;
    private final String surname;
    private final String address;
    private final String phoneNumber;
    private final String age;
}

Stringly-typed

public class Person {
    private final String name;
    private final Surname surname;
    private final Address address
    private final PhoneNumber phoneNumber;
    private final PositiveInteger age;
}

Strongly-typed

Optionality encapsulated

Person findOne(long id);

Optional<Person> findOne(long id);
sealed abstract class Try[+T]

final case class Success[+T](value: T) extends Try[T] { ... } 
final case class Failure[+T](exception: Throwable) extends Try[T] { ... }

Try in Scala

sealed -> all possible implementations were provided

public interface Try<T> { ... } 


final class Failure<T> implements Try<T> { ... }
final class Success<T> implements Try<T> { ... }

Try in Java(Vavr)

No sealed in Java  (╯°□°)╯︵ ┻━┻)

static <T> Try<T> of(CheckedFunction0<? extends T> supplier) {
        Objects.requireNonNull(supplier, "supplier is null");
        try {
            return new Success<>(supplier.apply());
        } catch (Throwable t) {
            return new Failure<>(t);
        }
    }

Try in Java(Vavr) - instantiation

Try<U> map(Function<? super T, ? extends U> mapper)
Try<U> flatMap(Function<? super T, ? extends Try<? extends U>> mapper)
Try<T> filter(Predicate<? super T> predicate) ...

Try<T> orElse(Try<? extends T> other)

Try<T> onFailure(Consumer<? super Throwable> action)
Try<T> onSuccess(Consumer<? super T> action)

Try<T> recover(Class<X> exception, T value)

T get(); // do not use that.

Try API

List<URL> getSearchResults(String searchString) throws IOException

Try in Action I - consumer side

Try<List<URL>> getSearchResults(String searchString) { ... }
getSearchResults("javaday")
 .onFailure(ex -> LOG.info("Houston, we have a problem:" + ex.getMessage()))
 .getOrElse(() -> Collections.singletonList(javaday));
List<URL> getFromGoogle(String search) throws NoSuchElementException, IOException

Try in Action II - producer side

List<URL> getFromDuckDuckGo(String search) throws IOException
static Try<List<URL>> getSearchResults(String searchString) {
    return Try.of(() -> getFromGoogle(searchString))
      .recover(NoSuchElementException.class, emptyList())
      .recover(NSAForbiddenException.class, emptyList())
      .orElse(() -> Try.of(() -> getFromDuckDuckGo(searchString)));
}
static Try<URL> getSingleSearchResults(String searchString)

Try in Action III - flatmap

 getSingleSearchResults("javaday")
  .map(url -> url.openStream()) //Unhandled exception
 Try<Try<InputStream>> javaday = getSingleSearchResults("javaday")
  .map(url -> Try.of(url::openStream));


 Try<InputStream> javaday = getSingleSearchResults("javaday")
  .flatMap(url -> Try.of(url::openStream))

Try in Action IV - pattern matching

Try<InputStream> javaday = getSingleSearchResults("javaday")
  .flatMap(url -> Try.of(url::openStream))



Match(javaday).of(
  Case($Success($()), "Opened successfully"),
  Case($Failure($()), "Failed :(")
);
Tuple2<Person, ErrorObject> findOne(long id);


Tuple2<Person, ErrorObject> res = findOne(42);
if (res._2 != null) { ... }
if (res._1 != null) { ... }

Omnipresent "Java" feel instead

Simulating Go's feel with Tuple

Tuples are expected to hold non-nullable values

sealed abstract class Either[+A, +B]


final case class Left[+A, +B](value: A) extends Either[A, B]
final case class Right[+A, +B](value: B) extends Either[A, B]

Either in Scala

  • Disjoint union
  • Try<T> is isomorphic to Either<Throwable, T>​
  • By convention, Right is success and Left is failure
  • In Scala, Either is right-biased now*
interface Either<L, R> 


final class Left<L, R> implements Either<L, R>
final class Right<L, R> implements Either<L, R>

Either in Java(Vavr)

Either.left("42");
Either.right(42);

Either instantiation

Separate factory methods for Left and Right:

LeftProjection<L, R> left()
RightProjection<L, R> right()

Either<X, Y> bimap(Function<..., X> leftMapper, Function<..., Y> rightMapper)

U fold(Function<..., U> leftMapper, Function<..., U> rightMapper)

R getOrElseGet(Function<? super L, ? extends R> other)

Either<R, L> swap()

L getLeft()  // do not use that unless you know what you are doing
L getRight() // yup, you guessed it.

Either API

case class FetchError(msg: String, response: HttpResponse)

Either in action - error object

public static class FetchError {
        private final String msg;
        private final HttpResponse response;

        public FetchError(String msg, HttpResponse response) {
            this.msg = msg;
            this.response = response;
        }

        public String getMsg() {
            return msg;
        }

        public HttpResponse getResponse() {
            return response;
        }

        @Override
        public String toString() {
            return "FetchError{" +
              "msg='" + msg + '\'' +
              ", response=" + response +
              '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            FetchError that = (FetchError) o;

            if (msg != null ? !msg.equals(that.msg) : that.msg != null) return false;
            return response != null ? response.equals(that.response) : that.response == null;
        }

        @Override
        public int hashCode() {
            int result = msg != null ? msg.hashCode() : 0;
            result = 31 * result + (response != null ? response.hashCode() : 0);
            return result;
        }
    }

Either in action - error object Java

Try<List<URL>> getSearchResults(String searchString)

Either<FetchError, List<URL>> getSearchResults(String searchString)
 getSearchResults("touk")
  .right()
  .filter(...)
  .map(...)
  .getOrElse(Collections::emptyList);


getSearchResults("touk")
  .left()
  .map(FetchError::getMsg)
  .forEach(System.out::println);

Either in action

public static Either<DnsUrl, URL> resolve(String url)

Modeling alternative paths

/** Returns either the next step of the tailcalling computation,
 * or the result if there are no more steps. */
@annotation.tailrec final def resume: Either[() => TailRec[A], A] = this match {
    case Done(a) => Right(a)
    case Call(k) => Left(k)
    case Cont(a, f) => a match {
      case Done(v) => f(v).resume
      case Call(k) => Left(() => k().flatMap(f))
      case Cont(b, g) => b.flatMap(x => g(x) flatMap f).resume
    }
}

Modeling alternative paths - Scala TailCalls

 

Absence modeling

Do we really need exceptions here? 

Person findOne(long id) throws NoSuchElementException;
Integer valueOf(String s) throws NumberFormatException;
Optional<Person> findOne(long id);
Optional<Integer> valueOf(String s);

Why not?

Absence modeling

What if your language does not support that?

  • Switch language.
  • Build it yourself.
  • Use vavr.io (formerly known as Javaslang)
  • Take part in Java Community Process

Key Takeaways

  • Exceptions work best when you do not expect people to recover from them
  • Try can be used for representing computations that may throw an exception
  • Absence can be modelled with Option
  • Either can be used for advanced scenarios involving error objects and modeling alternative paths
  • VAVR is the new Guava

"To Try or not to Try, there is no throws"

Thank You!

Yoda, ~41:3

Grzegorz Piwowarek @pivovarit

References:

Functional Error Handling Raúl Raja Martínez​

The Neophyte's Guide to Scala Daniel Westheide