Blog

Eine Alternative zu Ausnahmen in Java: Validierungen, Teil 1

Cristiana

Cristiana

Aktualisiert Oktober 21, 2025
7 Minuten

Ausnahmen in Java sind aufgrund ihrer besonderen Behandlung inkonsistent. Sie sind wie ein separater Informationsfluss, der nicht nur zusätzliche Ressourcen im Kopf des Entwicklers beansprucht, sondern auch eine Menge Standardcode und eine hohe Wahrscheinlichkeit für neue Fehler mit sich bringt. Anstelle von Ausnahmen können Fehler mit "Validierungen" behandelt werden. Das zugrundeliegende Konzept einer Validierung bietet einen effektiven und konsistenten Ansatz für die Fehlerbehandlung und Datenvalidierung. Obwohl Validierungen mehr als fähig sind, jede Art von Ausnahme zu behandeln, werde ich mich in diesem Beitrag auf die Validierung von Daten und Ergebnissen konzentrieren.

https://twitter.com/iamdevloper/status/1166699966263201792

Warum Validierungen

Eine Validierung ist ein Typ, der ein (gültiges) Ergebnis oder einen (ungültigen) Fehler darstellt. Er enthält nie beides (eine disjunkte Vereinigung); Ergebnisse werden mit und Fehler mit erstellt. Die Validation API in Vavr enthält eine detaillierte Beschreibung:

Die Validierungssteuerung ist ein applikativer Funktor und erleichtert das Sammeln von Fehlern. Wenn Sie versuchen, Monaden zu komponieren, wird der Kombinationsprozess beim ersten aufgetretenen Fehler einen Kurzschluss verursachen. Aber setzt die Verarbeitung der kombinierenden Funktionen fort und sammelt dabei alle Fehler. Dies ist besonders nützlich, wenn Sie mehrere Felder validieren, z.B. in einem Webformular, und Sie alle aufgetretenen Fehler wissen wollen, anstatt nur einen nach dem anderen.

Wenn Sie mit funktionalem Jargon nicht vertraut sind, mag diese Beschreibung zunächst ein wenig überwältigend klingen. In einfachem Englisch heißt das: Einkann viele Eigenschaften wie Namen, Postleitzahlen und Längen validieren, ohnebeim ersten Fehler aufzuhören. Noch cooler ist, dass die Validierungen, die nichtvoneinander abhängen, parallel durchgeführt werden können. Versuchen Sie das mal mit Ausnahmen! Einen guten Überblick über Validierungen finden Sie in Cats, der funktionalen Programmierbibliothek für Scala.

Randbemerkung
Validierungen sind von Either abgeleitet, das auch für die Ausnahmebehandlung verwendet werden kann. Der Vorteil von Validierungen ist, dass sie umfangreichere Funktionen für die Fehlerbehandlung bieten.

Zurück zu den Validierungen: Sie geben Ihnen die Werkzeuge an die Hand, mit denen Sie den "Fluss" fortsetzen können, der sonst durch Ausnahmen unterbrochen würde. Was ist also mit der Unterbrechung des Flusses gemeint?

public static Address validate(String street, String zipCode) {
    if (!validStreet(street)) {
        throw new IllegalArgumentException("street is not valid");
    } else if (!validZipCode(zipCode)) {
        throw new IllegalArgumentException("zipcode not valid");
    }  // ...
    return new Address(street, zipCode);
}

Der Aufruf von validate erfordert einen try/catch Block mit angemessener Ausnahmebehandlung. Wenn Sie diesen Block z.B. mit einer ungültigen Postleitzahl und einem ungültigen Ort aufrufen, erhalten Sie nur eine Ausnahme für die ungültige Postleitzahl, aber nicht für den Ort. Dieses Muster "Fehler - Fehlerbehebung - nächster Fehler - nächste Fehlerbehebung - ..." ist z.B. für Benutzerformulare nicht sehr praktisch. Die konsistente Implementierung der Ausnahmebehandlung in Vanilla Java kann eine schwierige Aufgabe sein.

Meine Mutter sagte immer: 'Das Leben ist wie eine Schachtel Pralinen. Du weißt nie, was du bekommst.'

- Forrest Gump

Validierungen nehmen diesen Schmerz und halten sich an das Prinzip der geringsten Überraschung: Sie halten den Vertrag ein, den eine Funktion mit ihrem Aufrufer hat. Sie werden vielleicht antworten: "Ja, aber in Java erwarte ich überall Ausnahmen, also bin ich nicht mehr überrascht". Ein Beispiel könnte den Vorteil verdeutlichen:

Seq<String> content;
try {
   content = jsonDocuments
      .map(Common::parseJson)
      .map(node -> node.get("url").asText())
      .map(Common::loadContent);
} catch (Exception ex) {
    // ...
}

Wenn nur ein Funktionsaufruf im obigen Beispiel eine Ausnahme auslöst, wird der gesamte Ablauf unterbrochen. Man muss mit möglichen Ausnahmen in parseJson und loadContent umgehen, was zusätzlichen Code erfordert. Das gleiche Beispiel mit Validierungen:

List<Validation<List<DomainError>, String>> validatedContent = jsonDocuments
   .map(Common::parseJsonValidated)
   .map(v -> v.map(node -> node.get("url").asText()))
   .map(v -> v.flatMap(Common::loadContentValidated))
   .filter(Validation::isValid);
Seq<String> content = Validation.sequence(validatedContent).get();

Hier ist der Fluss belastbar und die Ergebnisse können leicht extrahiert werden. Fehler werden analog behandelt (nicht gezeigt).

Im Gegensatz zu Ausnahmen, bei denen der Ausnahmetyp vom Aufrufer überprüft werden muss, kapseln Validierungen Fehler und ihre Typen ein. Eine zusätzliche Prüfung kann übersprungen werden. Diese Tatsache wird anhand einer Beispielfunktion deutlicher. Lassen Sie uns einen Ländernamen validieren und dessen CountryCode zurückgeben:

public static CountryCode validateCountry(String country) {
     for(CountryCode cc : CountryCode.values()) {
         if(cc.name().toLowerCase().equals(country.toLowerCase())) {
             return cc;
         }
     }
     throw new RuntimeException("could not find country code");
}

Aus der Signatur der Funktion lässt sich nicht ableiten, was passiert, wenn die Validierung nicht erfolgreich ist. In einfachem Java wissen Sie nie, was auf Sie zukommt: Sie können kaum vorhersagen, ob und welche Art von (Laufzeit-)Ausnahme während des Funktionsaufrufs auftaucht.

Implementierung

Dennoch haben Validierungen nicht nur Vorteile: Sie erfordern einen gewissen anfänglichen Aufwand, denn Sie müssen sich tatsächlich mit Fehlern vor Ort auseinandersetzen und können sie nicht mit catch-Blöcken wegdrücken. Ein systematischer Ansatz wird Sie jedoch auf lange Sicht beschleunigen. Wie sieht der Code aus? Nehmen wir an, wir sind das Backend einer Webanwendung und müssen Adressdaten für ein Webformular validieren:

public static Validation<Seq<DomainError>, Address>
validate(String street,
         String zipCode) {
   return Validation.combine(
           validateStreet(street),
           validateZipCode(zipCode)
   ).ap(Address::new);
}

Eine Validierungsfunktion nimmt Eingabewerte entgegen und gibt die korrekt eingegebene Eingabe zurück, die in diesem Fall Address ist. Die Funktion validate besteht aus separaten Funktionen. Jede Funktion prüft einen Teil der Adresse und gibt ein Validierungsobjekt zurück. In der Methode werden alle Überprüfungen zu einem -Objekt zusammengefasst, oder, wenn es Fehler gibt, stattdessen eine Liste von -Objekten. Eine Validierungsfunktion kann einfach die maximale Länge eines Eingabefeldes überprüfen:

private static final int MAX_STREET_LENGTH = 500;
private static Validation<DomainError, String> validateStreet(String street) {
      return street.length() > MAX_STREET_LENGTH ?
              Validation.invalid(new AddressError(
                      "street",
                      String.format("Street name must not be longer than %s", MAX_STREET_LENGTH))) :
              Validation.valid(street);
  }

Oder es ist möglich, Validierungsfunktionen aus Bibliotheken oder vorhandenem Code in Validierungen zu verpacken:

private static Validation<DomainError, CountryCode> validateCountry(String country) {
    return Try.of(() -> findCountryCode(country))
            .map(Validation::<DomainError, CountryCode>valid)
            .getOrElseGet(ex -> Validation.invalid(new DomainError(ex.getMessage())));
            // or alternatively (if the message can be neglected)
            //.getOrElse(Validation.invalid(new DomainError("Unknown country")));
}

Bei der Verwendung der Validierungskontrolle ist es oft von Vorteil, eine allgemeine DomainError Klasse einzuführen. In ihrer einfachsten Form könnte sie wie folgt aussehen:

import lombok.Getter;
@Getter
public class DomainError {
    protected String message;
    public DomainError(String message) {
        this.message = message;
    }
    public RuntimeException toException() {
        return new RuntimeException(this.message);
    }
}

Wenn Validierungen in großen Systemen verwendet werden, kann DomainError als Elternteil für spezifischere Fehler dienen, wie z.B. InvalidAddress oder UserNotFound.

Ganz am Ende, wenn das Ergebnis oder die Fehlermeldung an einen Dienst oder Benutzer gesendet wird, werden die Validierungen ausgepackt (oder im funktionalen Slang: gefaltet).

public Result getUserMail(String userId) {
    return userService
        .getUser(userId)
        .map(User::getEmail)
        .fold(
            error -> status(Http.Status.BAD_REQUEST, error.toString()),
            email -> status(Http.Status.OK, email)
    );
}

Natürlich und wenn möglich, können Validierungen auch schon früher gelöst werden.

Schützen Sie Ihre Kontext-Grenze

Dieser Ansatz passt gut zu den Konzepten des Domain-driven Design:

Innerhalb der Kontext- (oder Microservice-) Grenze werden Validierungen mit Domänenfehlern, z.B. Validation<InvalidCredentials, User>, verwendet. Wenn möglich, können Fehler an Ort und Stelle behoben werden. Wenn nicht, sollten Validierungsfehler erst beim Verlassen der Kontextgrenze in entsprechende Fehler umgewandelt werden, z.B. in eine HTTP 403 - forbidden Antwort. Dies ist ein sehr leistungsfähiger Ansatz, da Implementierungsdetails in der Adapterschicht (siehe Abbildung) nicht in Ihren begrenzten Kontext durchsickern. Sie könnten das Framework, das Sie im RESTful-Adapter verwenden, von Spring auf Play umstellen, ohne den Code innerhalb des Bounded Contexts, d.h. Ihren reinen Domänencode, anzutasten.

Fazit

Validierungen bieten Ihnen eine strukturierte und konsistente Möglichkeit, mit Fehlern in einer Domäne umzugehen. Dieser Beitrag ist ein Rundgang durch die wichtigsten Konzepte und Vorteile. Beachten Sie, dass es sich hierbei nicht um ein Schwarz-Weiß-Thema handelt. Es ist also absolut in Ordnung, wenn Ausnahmen und Validierungen nebeneinander existieren. Bestes Beispiel ist Lagom, wo Ausnahmen von -Klassen zurückgegeben werden. In diesem Fall ist es vielleicht besser, sich an den vom Framework vorgeschlagenen Ansatz zu halten oder einen hybriden Ansatz zu wählen.

Viel Spaß!

(Sie können diesen Beitrag auch in meinem persönlichen Blog finden)

Verfasst von

Cristiana

Some bio goes here

Contact

Let’s discuss how we can support your journey.