Elegantes Exception Handling mit der Try-Monade

Elegantes Exception Handling mit der Try-Monade

Problem

Für eine robuste Software ist der sorgfältige Umgang mit Exceptions unumgänglich. Gängige Strategien sind entweder lokales Behandeln mit try{}catch(SpecificException ex){} oder zentrales Behandeln. Letzteres bedeutet, dass Exceptions nach oben gegeben und zentral mit mehreren catch-Blöcken behandelt werden. Lokales Behandeln stört die Lesbarkeit des Programmflusses. Besonders unbeliebt sind Checked Exceptions, da diese lokales Behandeln erzwingen oder mit der throws-Klausel explizit nach oben gegeben werden müssen.

In Java hat dies zu Entwicklungen wie Lombok's @SneakyThrows geführt. Dabei werden Checked Exceptions per Code-Generierung in Runtime Exceptions überführt. Konzeptionell ist zentrales Behandeln jedoch nicht weit entfernt von GOTO, da der eigentliche Programmfluss abgebrochen werden muss. In der Praxis findet man meist eine Kombination aus beiden Ansätzen. In gruseligen Fällen werden sogar neue Programmflüsse aus zentral behandelten Exceptions heraus gestartet.

Lösung

In den letzten Jahren haben verschiedene Konzepte der funktionalen Programmierung Einzug in den Mainstream der Softwareentwicklung gehalten. Beispiele in Java sind Lambda-Ausdrücke oder die Monaden Optional und Stream. Für Exception Handling gibt es in der Welt der funktionalen Programmierung das Konzept Railway Oriented Programming mit Monaden wie Try oder Result.

Mit der Bibliothek VAVR kann man Java einfach um die Try-Monade erweitern. Ihre Verwendung ist nicht komplizierter als die (richtige) Verwendung von Optional. Hier wird mit Try.of(() -> ...) die Ausführung einer Funktion in einen monadischen Kontext Try<T> überführt, der entweder Success<T> oder Failure sein kann. Dabei wird entweder das Ergebnis der Funktion (Typ T) oder ein Throwable gespeichert, das später behandelt werden kann. Zudem können mit try.map(x -> ...) weitere Operationen auf den inneren Wert angewendet werden, welche nur im Kontext Success ausgeführt wird. Ist vorab schon eine Exception aufgetreten, so wird diese bei map einfach "weitergereicht". Analog dazu, können mit mapFailure auch Operationen auf dem Throwable ausgeführt oder mit flatMap der Kontext gewechselt werden.

Beispiel

Success 
Try<Integer> myTry = Try
 .of(() -> 10 / 2)
 .map(i -> i + 1);
assertTrue(myTry.isSuccess());
myTry.onSuccess(
 i -> assertEquals(6, i));


Failure
Try<Integer> myTry = Try
 .of(() -> 10 / 0)
 .map(i -> i + 1);

assertTrue(myTry.isFailure());
myTry.onFailure(
  ex -> assertTrue(
    ex instanceof
      ArithmeticException));
From Failure to Success
// Generally
Try<Integer> try2 = myTry
  .orElse(() -> 0)
// For specific exceptions
Try<Integer> try2 = myTry.recover(
  ArithmeticException.class,
  ex -> 0)


From Success to Failure
Try.of(() -> send(request))
   .flatMap(resp -> resp.isEmpty()
     ? Try.failure(new MyExptn())
     : Try.success(resp));


Access to inner value
T t = myTry.getOrElseGet(ex -> ...)
Either<Throwable, T> =
  myTry.toEither()
Exception Handling in Streams
List<Object> input = List.of(...);
List<Try<Object>> trys =
  input.stream()
       .map(o ->
         Try.of(() -> o)
            .map(this::validate)
            .map(this::convert)
            .map(this::saveToDb))
       .collect(
         Collectors.toList());
trys.stream()
    .filter(Try::isFailure)
    .forEach(
      t -> t.onFailure(log::warn));
List<Object> output =
  trys.stream()
      .filter(Try::isSuccess)
      .map(t -> t.getOrElseGet(
        ex -> ...))
      .collect(Collectors.toList());

Weiterführende Aspekte

  • Im VAVR user guide findet ihr leider nur eine kurze Abhandlung, einen besseren Guide gibt es bei Baeldung.
  • Am besten werft ihr einen Blick in den gut dokumentierten Quellcode!
  • Das generelle Konzept von Railway Oriented Programming wird in diesem Artikel gut erklärt.
  • Eine weitere Try-Implementierung findet ihr in Scala.

---

Autor: André Petermann / Software Architect / Standort Leipzig

Elegant Exception Handling Using the Try Monad

Wir verwenden Cookies, um unsere Webseite für Sie zu optimieren. Mit dem Besuch unserer Webseite erklären Sie sich damit einverstanden. // Our website is using cookies to improve your experience. By continuing to browse the site, you are agreeing to our use of cookies.

Weitere Informationen finden Sie in unserer Datenschutzerklärung. // For more information, please refer to our privacy policy.