Stufe 3Objekte und Typen · Lektion 18 von 34
Exception Handling
Scala übernimmt das Ausnahmesystem der JVM, schreibt es aber knapper: catch arbeitet mit Pattern Matching, try ist ein Ausdruck, und es gibt keine geprüften Ausnahmen. Trotzdem setzt idiomatischer Scala-Code Exceptions sparsam ein.
Ausnahmen werfen
def wurzel(x: Double): Double =
if x < 0 then throw IllegalArgumentException(s"negativ: $x")
else math.sqrt(x)throw ist ein Ausdruck vom Typ Nothing. Weil Nothing ein Untertyp jedes anderen Typs ist, passt er in jeden Zweig – der Rückgabetyp von wurzel bleibt Double. Ausnahmen sind gewöhnliche Java-Klassen; Sie können die vorhandenen nutzen oder eigene von Exception ableiten.
Ausnahmen fangen
import java.io.{FileNotFoundException, IOException}
def lies(pfad: String): String =
try
scala.io.Source.fromFile(pfad).mkString
catch
case e: FileNotFoundException => s"Datei fehlt: ${e.getMessage}"
case e: IOException => s"Lesefehler: ${e.getMessage}"
finally
println("Leseversuch beendet")Der catch-Block ist eine Folge von case-Fällen wie beim Pattern Matching. Die Fälle werden der Reihe nach geprüft; der speziellere gehört deshalb nach oben. finally läuft immer – egal ob ein Fehler auftrat oder nicht – und eignet sich für Aufräumarbeiten. Sein Wert wird verworfen.
try ist ein Ausdruck
Wie if und match liefert auch try einen Wert:
val port =
try sys.env("PORT").toInt
catch case _: NoSuchElementException | _: NumberFormatException => 8080Mehrere Ausnahmetypen lassen sich mit | zusammenfassen.
Keine geprüften Ausnahmen
Java unterscheidet zwischen geprüften Ausnahmen (checked exceptions), die in der Methodensignatur stehen müssen, und ungeprüften. Scala kennt diese Unterscheidung nicht: Keine Ausnahme muss deklariert werden. Das spart Schreibarbeit, hat aber einen Preis – am Typ einer Methode sieht man nicht, ob sie scheitern kann. Wer das dokumentieren möchte, kann die Annotation @throws[IOException] verwenden; sie ist vor allem für Aufrufer aus Java gedacht.
Nicht alles fangen
Ein case _: Throwable oder case e => … fängt wirklich alles, auch schwere Fehler wie OutOfMemoryError oder Unterbrechungen von Threads. Verwenden Sie stattdessen NonFatal:
import scala.util.control.NonFatal
try riskant()
catch case NonFatal(e) => println(s"Fehler: ${e.getMessage}")NonFatal passt auf alle „normalen“ Ausnahmen und lässt Fehler durch, bei denen ein Weiterlaufen ohnehin sinnlos wäre.
Ressourcen sicher schließen: Using
Dateien, Datenbankverbindungen und Netzwerk-Sockets müssen geschlossen werden – auch im Fehlerfall. Statt verschachtelter try/finally-Blöcke gibt es scala.util.Using:
import scala.util.Using
import scala.io.Source
val zeilen: scala.util.Try[Int] =
Using(Source.fromFile("daten.csv")) { quelle =>
quelle.getLines().count(_.nonEmpty)
}Using schließt die Ressource automatisch und liefert das Ergebnis als Try: Success(anzahl) oder Failure(ausnahme). Für mehrere Ressourcen gibt es Using.Manager. Das entspricht dem try-with-resources aus Java.
Eigene Ausnahmen
final class UngueltigeBestellung(nachricht: String) extends Exception(nachricht)
def pruefe(menge: Int): Unit =
if menge <= 0 then throw UngueltigeBestellung(s"Menge $menge ist nicht erlaubt")Wann Exceptions – und wann nicht?
In funktionalem Scala-Code gelten Exceptions als Mittel für unerwartete Fehler: Programmierfehler, kaputte Konfiguration, ausgefallene Infrastruktur. Für erwartbare Fälle – ein Benutzer gibt keine Zahl ein, ein Datensatz existiert nicht – nutzt man lieber Werte, die den Fehlerfall im Typ sichtbar machen:
| Situation | Werkzeug |
|---|---|
| Wert kann fehlen | Option |
| Aufruf kann mit Exception scheitern | Try |
| Fehler mit eigener Beschreibung | Either |
| wirklich unerwarteter Fehler | Exception |
Der Vorteil: Wer eine Methode mit dem Rückgabetyp Either[Fehler, Kunde] aufruft, muss sich um beide Fälle kümmern – der Compiler erinnert daran. Eine Exception dagegen fliegt still durch alle Schichten, bis jemand sie fängt, oder bringt das Programm zum Absturz.