Fehlerbehandlung in Swift
In Swift ist ein Fehler ein Wert, der das Protokoll Error erfüllt, meist ein Enum. Eine Funktion, die scheitern kann, ist mit throws markiert, und jeder Aufruf braucht try. Mit do und catch behandelst du Fehler, mit try? machst du daraus nil. Seit Swift 6 kannst du mit throws(Typ) festlegen, welche Fehler eine Funktion werfen darf.
Danach kannst du
- definierst eigene Fehler als Enum
- wirfst, gibst weiter und fängst Fehler mit throws, try, do und catch
- nutzt typisierte Fehler mit throws(Typ)
- wählst zwischen throws, Result und Optional
Voraussetzungen
Umgebung
Swift 6.4.0, Sprachmodus 6
plattformunabhängig, geprüft unter Linux x86-64
Typisierte Fehler (throws(Typ)) gibt es seit Swift 6.0.
Auf dieser Seite
Fehler sind Werte
Swift unterscheidet zwei Arten von Problemen. Programmierfehler wie ein ungültiger Array-Index oder ein ! auf nil beenden das Programm sofort; du hast sie in den vorigen Lektionen gesehen. Erwartbare Fehler dagegen gehören zum normalen Ablauf: Eine Datei fehlt, eine Eingabe ist ungültig, das Netz ist weg. Für sie gibt es throws.
Ein Fehler ist ein beliebiger Typ, der das Protokoll Error erfüllt. Meist ist das ein Enum, weil du damit alle Fehlerfälle einer Funktion an einer Stelle aufzählst und jedem Fall passende Werte mitgeben kannst.
enum AnmeldeFehler: Error {
case leererName
case zuKurz(mindestens: Int)
}
func pruefe(benutzername: String) throws(AnmeldeFehler) -> String {
if benutzername.isEmpty {
throw .leererName
}
if benutzername.count < 3 {
throw .zuKurz(mindestens: 3)
}
return benutzername.lowercased()
}
for eingabe in ["Ada", "", "Al"] {
do {
let name = try pruefe(benutzername: eingabe)
print("ok:", name)
} catch .leererName {
print("Fehler: Bitte gib einen Namen ein.")
} catch .zuKurz(let mindestens) {
print("Fehler: mindestens \(mindestens) Zeichen.")
}
}
// try? macht aus einem Fehler einfach nil
let vielleicht = try? pruefe(benutzername: "")
print(vielleicht as Any)
Ausgabe
ok: ada Fehler: Bitte gib einen Namen ein. Fehler: mindestens 3 Zeichen. nil
Der Ablauf:
throws(AnmeldeFehler)in der Signatur sagt: Diese Funktion kann scheitern, und zwar nur mit einemAnmeldeFehler.throw .leererNamebeendet die Funktion sofort mit diesem Fehler. Keinreturnnötig.- Beim Aufruf steht
try. Es markiert für Leser jede Stelle, an der der Ablauf abbrechen kann. doumschließt den Code,catchbehandelt die Fehler. Weil der Fehlertyp feststeht, kannst du direkt auf die Fälle prüfen:catch .zuKurz(let mindestens).try?macht aus einem Fehler einfachnil. Praktisch, wenn dich der Grund nicht interessiert.
Typisierte und untypisierte Fehler
Ohne Typangabe schreibst du nur throws. Dann darf die Funktion jeden beliebigen Error werfen, und der catch-Block bekommt ein allgemeines error, das du mit catch let fehler as AnmeldeFehler oder catch is AnmeldeFehler erst wieder einordnen musst. Typisierte Fehler mit throws(Typ) gibt es seit Swift 6.0 (SE-0413).
Welche Variante ist besser? Der Vorschlag selbst empfiehlt typisierte Fehler vor allem für Code, bei dem die Menge der Fehler fest und abgeschlossen ist, etwa innerhalb eines Moduls oder bei kleinen, klar umrissenen Funktionen wie pruefe. Für öffentliche Schnittstellen, die sich weiterentwickeln, ist das einfache throws oft flexibler, weil neue Fehlerarten dann keinen Bruch der Schnittstelle bedeuten.
Vergisst du try, übersetzt der Compiler den Code nicht und bietet gleich drei Lösungen an:
enum NetzFehler: Error { case offline }
func lade() throws -> String {
throw NetzFehler.offline
}
let text = lade()
Compiler-Meldung
try-fehlt.swift:7:12: error: call can throw but is not marked with 'try' 5 | } 6 | 7 | let text = lade() | |- error: call can throw but is not marked with 'try' | |- note: did you mean to use 'try'? | |- note: did you mean to handle error as optional value? | `- note: did you mean to disable error propagation? 8 |
Die drei note-Zeilen entsprechen try (Fehler weitergeben), try? (Fehler zu nil) und try! (Fehler zum Absturz). Die dritte ist wie das ! bei Optionals nur vertretbar, wenn ein Fehler wirklich unmöglich ist.
Fehler weitergeben
Eine Funktion, die selbst throws ist, muss Fehler nicht fangen. Sie schreibt try vor den Aufruf und gibt einen Fehler automatisch an ihren eigenen Aufrufer weiter. So wandern Fehler bis zu der Stelle, die sinnvoll reagieren kann, zum Beispiel bis zur Oberfläche, die eine Meldung anzeigt. Mit defer { … } legst du Code fest, der beim Verlassen eines Blocks immer läuft, auch bei einem Fehler: ideal zum Aufräumen.
Result: Erfolg oder Fehler als Wert
Result<Wert, Fehler> ist ein Enum mit den Fällen .success und .failure. Es speichert das Ergebnis eines Vorgangs, der scheitern kann, als normalen Wert:
enum ParseFehler: Error, CustomStringConvertible {
case keineZahl(String)
case negativ(Int)
var description: String {
switch self {
case .keineZahl(let text): "„\(text)“ ist keine Zahl"
case .negativ(let wert): "\(wert) ist negativ"
}
}
}
func menge(aus text: String) -> Result<Int, ParseFehler> {
guard let wert = Int(text) else { return .failure(.keineZahl(text)) }
guard wert >= 0 else { return .failure(.negativ(wert)) }
return .success(wert)
}
for text in ["12", "zwölf", "-3"] {
switch menge(aus: text) {
case .success(let wert):
print("Menge:", wert)
case .failure(let fehler):
print("Fehler:", fehler)
}
}
// Result und throws lassen sich ineinander umwandeln
let doppelt = try? menge(aus: "21").map { $0 * 2 }.get()
print(doppelt ?? 0)
Ausgabe
Menge: 12 Fehler: „zwölf“ ist keine Zahl Fehler: -3 ist negativ 42
Mit switch behandelst du beide Fälle. map wandelt den Erfolgswert um und reicht einen Fehler unverändert durch. get() verwandelt ein Result zurück in einen werfenden Aufruf, deshalb steht davor try?.
CustomStringConvertible sorgt dafür, dass print den Fehler als lesbaren deutschen Satz ausgibt. Diese Kombination aus Enum und description lohnt sich für jeden Fehler, den Menschen zu sehen bekommen.
throws, Result oder Optional?
| Situation | Nimm |
|---|---|
| Ein Wert kann fehlen, der Grund ist egal oder offensichtlich | Optional (Int("x") liefert nil) |
| Ein Vorgang kann aus verschiedenen Gründen scheitern, der Aufrufer soll reagieren | throws |
| Das Ergebnis soll gespeichert, gesammelt oder später ausgewertet werden | Result |
| Der Fehler ist ein Programmierfehler, der nie passieren darf | precondition oder fatalError |
Mit async/await ist throws der übliche Weg geworden, auch für Netzwerkfehler. Das siehst du in der nächsten Lektion zu async/await.
Häufige Fehler
call can throw but is not marked with 'try'. Vor einem werfenden Aufruf fehlt try.
errors thrown from here are not handled. Du rufst mit try auf, bist aber weder in einem do-Block noch in einer throws-Funktion. Fange den Fehler oder markiere die Funktion mit throws.
Ein catch-Block fängt nie etwas. Bei untypisiertem throws prüft catch AnmeldeFehler.leererName nur diesen einen Fall. Ein abschließendes catch ohne Muster fängt den Rest.
Übungen
- Schreibe eine Funktion
teile(_ a: Int, durch b: Int) throws(RechenFehler) -> Int, die beib == 0einen Fehler.divisionDurchNullwirft. Rufe sie mit10, 2und1, 0auf und gib jeweils Ergebnis oder Fehlermeldung aus. - Wandle die Funktion aus Übung 1 in eine Variante um, die
Result<Int, RechenFehler>zurückgibt, und summiere mitcompactMapundtry?alle erfolgreichen Ergebnisse von[(10, 2), (3, 0), (9, 3)].
Lösung zu Übung 1
enum RechenFehler: Error {
case divisionDurchNull
}
func teile(_ a: Int, durch b: Int) throws(RechenFehler) -> Int {
guard b != 0 else { throw .divisionDurchNull }
return a / b
}
for (a, b) in [(10, 2), (1, 0)] {
do {
print("\(a) / \(b) = \(try teile(a, durch: b))")
} catch {
print("\(a) / \(b): Division durch null ist nicht erlaubt.")
}
}
Ausgabe
10 / 2 = 5 1 / 0: Division durch null ist nicht erlaubt.
Lösung zu Übung 2
enum RechenFehler: Error {
case divisionDurchNull
}
func teile(_ a: Int, durch b: Int) -> Result<Int, RechenFehler> {
b == 0 ? .failure(.divisionDurchNull) : .success(a / b)
}
let ergebnisse = [(10, 2), (3, 0), (9, 3)].map { teile($0.0, durch: $0.1) }
let summe = ergebnisse.compactMap { try? $0.get() }.reduce(0, +)
print(summe)
Ausgabe
8
try? $0.get() macht aus jedem Fehlschlag nil, und compactMap lässt diese weg. Übrig bleiben 5 und 3.