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."
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