Lernziel
Nach dieser Lektion kannst du Funktionen schreiben, die Fehler werfen (throws), eigene Fehlertypen mit Enums definieren und Fehler mit do, try und catch abfangen. Außerdem kennst du try? und weißt, wann ein Optional und wann ein Fehler das bessere Werkzeug ist.
Das Problem: „Hat nicht geklappt“ reicht nicht
Aus den Lektionen 1.8 bis 1.10 kennst du Optionals als Antwort auf „hat nicht geklappt“. Int("abc") liefert nil. Das funktioniert gut, solange es nur einen Grund für ein Scheitern gibt.
Aber stell dir eine Registrierung vor. Ein Benutzername kann aus vielen Gründen ungültig sein:
- zu kurz
- schon vergeben
- enthält Leerzeichen
Gibt deine Prüffunktion nur nil zurück, weiß der Nutzer nicht, was er falsch gemacht hat. Und später, beim Laden aus dem Internet, gibt es noch mehr Gründe: kein Netz, Server nicht erreichbar, falsche Daten.
Du brauchst einen Weg, um nicht nur „hat nicht geklappt“ zu sagen, sondern warum. Dafür gibt es in Swift die Fehlerbehandlung.
Schritt 1: Fehler beschreiben
Zuerst legst du fest, welche Fehler es geben kann. Am besten eignet sich dafür ein Enum (Lektion 1.14), das das Protokoll Error erfüllt:
enum UsernameError: Error {
case tooShort
case containsSpaces
case alreadyTaken
}Error ist ein Protokoll ohne Anforderungen. Es markiert einen Typ nur als „Das ist ein Fehler“. Mehr ist nicht nötig.
Schritt 2: Fehler werfen
Eine Funktion, die scheitern kann, markierst du mit throws. Tritt ein Problem auf, wirfst du einen Fehler mit throw:
func validate(username: String) throws -> String {
if username.count < 3 {
throw UsernameError.tooShort
}
if username.contains(" ") {
throw UsernameError.containsSpaces
}
if username.lowercased() == "admin" {
throw UsernameError.alreadyTaken
}
return username
}throwssteht nach den Parametern und vor dem Pfeil->.throwbeendet die Funktion sofort, ähnlich wiereturn, liefert aber einen Fehler statt eines Ergebnisses.
Statt if kannst du natürlich auch guard verwenden. Das ist hier sogar besonders elegant:
guard username.count >= 3 else {
throw UsernameError.tooShort
}Erinnerst du dich an die Fehlermeldung zu guard in Lektion 1.9? Dort hieß es „consider using a 'return' or 'throw'“. Jetzt weißt du, was throw ist.
Schritt 3: Fehler abfangen
Eine Funktion mit throws kannst du nicht einfach aufrufen:
let name = validate(username: "Al")Fehlermeldung
Call can throw but is not marked with 'try'
Swift zwingt dich, dich um mögliche Fehler zu kümmern, genau wie bei Optionals. Dafür gibt es do, try und catch:
do {
let name = try validate(username: "Al")
print("Willkommen, \(name)!")
} catch {
print("Fehler: \(error)")
}
// Fehler: tooShortSo liest du das:
do { ... }: „Versuch Folgendes.“try: markiert die Stelle, an der ein Fehler auftreten kann. Tritt einer auf, springt Swift sofort in dencatch-Block. Die restlichen Zeilen imdo-Block werden übersprungen.catch { ... }: „Falls es schiefgeht, mach das.“ Innerhalb voncatchsteht der Fehler automatisch in einer Konstante namenserror.
Erwartetes Ergebnis
Mit "Al" steht in der Konsole: Fehler: tooShort
Mit "anna" steht dort: Willkommen, anna!
Fehler einzeln behandeln
Willst du je nach Fehler anders reagieren, schreibst du mehrere catch-Blöcke:
do {
let name = try validate(username: "anna maria")
print("Willkommen, \(name)!")
} catch UsernameError.tooShort {
print("Der Name muss mindestens 3 Zeichen haben.")
} catch UsernameError.containsSpaces {
print("Bitte keine Leerzeichen verwenden.")
} catch UsernameError.alreadyTaken {
print("Dieser Name ist leider schon vergeben.")
} catch {
print("Unbekannter Fehler: \(error)")
}Das letzte catch ohne Muster fängt alles ab, was vorher nicht gepasst hat. Swift verlangt es, weil eine throws-Funktion theoretisch jeden Fehlertyp werfen kann.
Verständliche Fehlermeldungen
tooShort ist für Entwickler verständlich, für Nutzer nicht. Mit einer berechneten Eigenschaft gibst du jedem Fehler einen lesbaren Text:
enum UsernameError: Error {
case tooShort
case containsSpaces
case alreadyTaken
var message: String {
switch self {
case .tooShort: "Der Name muss mindestens 3 Zeichen haben."
case .containsSpaces: "Bitte keine Leerzeichen verwenden."
case .alreadyTaken: "Dieser Name ist leider schon vergeben."
}
}
}
do {
let name = try validate(username: "admin")
print("Willkommen, \(name)!")
} catch let error as UsernameError {
print(error.message)
} catch {
print("Unbekannter Fehler.")
}
// Dieser Name ist leider schon vergeben.catch let error as UsernameError heißt: „Fang alle Fehler vom Typ UsernameError und gib ihn mir als genau diesen Typ.“ Dann kannst du auf .message zugreifen.
Und Associated Values funktionieren natürlich auch. Ein Fehler kann so Zusatzinformationen mitbringen:
enum UsernameError: Error {
case tooShort(minimum: Int)
}
throw UsernameError.tooShort(minimum: 3)try?: Fehler in ein Optional verwandeln
Manchmal ist dir egal, warum etwas gescheitert ist. Dann gibt es eine Abkürzung:
let name = try? validate(username: "Al")
print(name) // niltry? wandelt das Ergebnis in ein Optional um: Klappt alles, bekommst du den Wert, sonst nil. Der Fehler selbst geht dabei verloren.
Das passt perfekt zu den Werkzeugen aus Lektion 1.9:
let displayName = (try? validate(username: input)) ?? "Gast"
if let name = try? validate(username: input) {
print("Gültig: \(name)")
}try! gibt es auch, aber …
Genau wie bei Optionals gibt es ein try!, das abstürzt, wenn ein Fehler auftritt. Für diesen Kurs gilt dasselbe wie für !: nicht verwenden.
Fehler weiterreichen
Ruft eine Funktion eine andere throws-Funktion auf, muss sie den Fehler nicht selbst behandeln. Sie kann ihn auch weiterreichen, indem sie selbst throws ist:
func register(username: String) throws {
let validName = try validate(username: username)
print("\(validName) wurde registriert.")
}Tritt in validate ein Fehler auf, beendet er auch register und landet beim Aufrufer von register. Irgendwo weiter oben muss ihn dann jemand mit do/catch abfangen.
So kannst du Fehler genau dort behandeln, wo es sinnvoll ist. In einer App ist das meist die Stelle, die dem Nutzer eine Meldung anzeigen kann.
Optional oder Fehler?
| Situation | Werkzeug |
|---|---|
| Es gibt einen offensichtlichen Grund für „kein Ergebnis“ | Optional (Int("abc"), array.first) |
| Es gibt mehrere Gründe, und der Aufrufer soll sie unterscheiden können | Fehler (throws) |
| Der Grund soll dem Nutzer angezeigt werden | Fehler mit verständlicher Meldung |
| Etwas, das grundsätzlich schiefgehen kann: Netzwerk, Dateien, Datenbanken | Fehler. Apples Funktionen dafür sind fast alle throws |
Tipp
Ab Stufe 3 wirst du ständig mit try arbeiten: Daten aus dem Internet laden (Lektion 3.10), JSON umwandeln (Lektion 3.9), Daten mit SwiftData speichern (Lektion 3.14). Das Muster aus dieser Lektion bleibt dabei immer gleich.
Typische Fehler
try vergessen
Fehlermeldung
Call can throw but is not marked with 'try'
Setz try vor den Aufruf und schließ ihn in do/catch ein, oder verwende try?.
try ohne do
let name = try validate(username: "anna")Fehlermeldung
Errors thrown from here are not handled
Ein try muss entweder in einem do-Block stehen oder in einer Funktion, die selbst throws ist.
throws vergessen
func validate(username: String) -> String {
throw UsernameError.tooShort
}Fehlermeldung
Error is not handled because the enclosing function is not declared 'throws'
Eine Funktion, die throw verwendet, muss mit throws markiert sein.
Fehlertyp erfüllt Error nicht
enum UsernameError {
case tooShort
}
throw UsernameError.tooShortFehlermeldung
Thrown expression type 'UsernameError' does not conform to 'Error'
Ergänze : Error beim Enum.
FAQ
Anfänger
Was bedeutet das überhaupt?Ist ein geworfener Fehler dasselbe wie ein Absturz?
Nein, ganz im Gegenteil. Ein geworfener Fehler ist ein kontrollierter Weg, ein Problem zu melden. Die App läuft weiter, und du entscheidest, wie du reagierst. Ein Absturz passiert, wenn etwas völlig Unerwartetes geschieht, etwa bei ! mit nil.
Muss ich für jede Funktion Fehlerbehandlung schreiben?
Nein. Nur Funktionen, die tatsächlich auf vorhersehbare Weise scheitern können, sollten throws sein. Eine Funktion, die zwei Zahlen addiert, braucht das nicht.
Woher weiß ich, ob eine Apple-Funktion throws ist?
Xcode zeigt es dir bei der Autovervollständigung an, und der Compiler meldet sich, wenn du try vergisst. Du kannst es also nicht übersehen.
Fortgeschritten
Warum verhält es sich so?Kann ich mehrere try in einem do-Block verwenden?
Ja. Der erste, der scheitert, beendet den Block und springt in catch. Die folgenden werden nicht mehr ausgeführt.
Wie gebe ich einem Fehler eine Meldung, die Apple-Komponenten automatisch verstehen?
Mit dem Protokoll LocalizedError und seiner Eigenschaft errorDescription. Dann liefert error.localizedDescription deinen Text, auch wenn der Fehler nur als allgemeines Error vorliegt. Das ist praktisch für Alerts in SwiftUI (Lektion 2.20).
Was ist defer?
Ein Block, der garantiert am Ende des aktuellen Bereichs ausgeführt wird, egal ob normal oder durch einen Fehler. Typisch für Aufräumarbeiten: defer { isLoading = false } setzt eine Ladeanzeige in jedem Fall zurück.
Profi
Was passiert unter der Haube?Was sind „typed throws“?
Seit Swift 6 kannst du angeben, welchen Fehlertyp eine Funktion wirft: func validate(username: String) throws(UsernameError) -> String. Dann weiß der Compiler genau, welche Fehler möglich sind, und ein abschließendes allgemeines catch ist nicht mehr nötig. Für App-Code empfiehlt Apple trotzdem meist das normale throws, weil sich Fehlertypen im Laufe der Zeit gern ändern.
Ist Fehlerbehandlung in Swift langsam?
Nein. Anders als „Exceptions“ in manchen anderen Sprachen ist throw in Swift im Grunde nur eine besondere Art von Rückgabewert. Es gibt kein teures „Abwickeln“ des Aufrufstapels.
Was ist Result?
Ein Enum der Standardbibliothek mit den Fällen .success(Wert) und .failure(Fehler). Man nutzte es vor allem vor async/await, um Ergebnisse von Hintergrundaufgaben weiterzugeben. Heute brauchst du es seltener, solltest es aber in älterem Code erkennen.
Übungen
Übung 1: Alter prüfen
Definiere einen Fehler AgeError mit den Fällen negative und unrealistic. Schreib eine Funktion checkAge(_:) throws, die bei Werten unter 0 bzw. über 130 den passenden Fehler wirft. Ruf sie mit do/catch für die Werte 25, -3 und 200 auf.
Lösung Übung 1Erst selbst probieren!
enum AgeError: Error {
case negative
case unrealistic
}
func checkAge(_ age: Int) throws {
guard age >= 0 else { throw AgeError.negative }
guard age <= 130 else { throw AgeError.unrealistic }
print("\(age) ist ein gültiges Alter.")
}
for age in [25, -3, 200] {
do {
try checkAge(age)
} catch {
print("\(age): \(error)")
}
}Übung 2: Geldautomat
Baue eine Funktion withdraw(amount:from:) throws -> Double, die vom Kontostand abhebt und den neuen Stand zurückgibt. Wirf insufficientFunds(missing: Double), wenn zu wenig Geld da ist, und invalidAmount, wenn der Betrag nicht positiv ist. Gib bei fehlendem Geld aus, wie viel fehlt.
Lösung Übung 2Erst selbst probieren!
enum ATMError: Error {
case invalidAmount
case insufficientFunds(missing: Double)
}
func withdraw(amount: Double, from balance: Double) throws -> Double {
guard amount > 0 else {
throw ATMError.invalidAmount
}
guard amount <= balance else {
throw ATMError.insufficientFunds(missing: amount - balance)
}
return balance - amount
}
do {
let newBalance = try withdraw(amount: 150, from: 100)
print("Neuer Kontostand: \(newBalance)")
} catch ATMError.insufficientFunds(let missing) {
print("Es fehlen \(missing) €.")
} catch ATMError.invalidAmount {
print("Bitte einen positiven Betrag eingeben.")
} catch {
print("Unbekannter Fehler.")
}
// Es fehlen 50.0 €.Übung 3: Formular prüfen
Schreib eine Funktion validateForm(email:password:) throws, die zwei andere throws-Funktionen aufruft: validateEmail(_:) (muss ein @ und einen Punkt enthalten) und validatePassword(_:) (mindestens 8 Zeichen). Beide verwenden ein gemeinsames Enum FormError mit einer message-Eigenschaft. validateForm soll die Fehler nicht selbst behandeln, sondern weiterreichen. Fang sie an einer Stelle ab und gib die Meldung aus.
Lösung Übung 3Erst selbst probieren!
enum FormError: Error {
case invalidEmail
case passwordTooShort
var message: String {
switch self {
case .invalidEmail: "Bitte eine gültige E-Mail-Adresse eingeben."
case .passwordTooShort: "Das Passwort braucht mindestens 8 Zeichen."
}
}
}
func validateEmail(_ email: String) throws {
guard email.contains("@"), email.contains(".") else {
throw FormError.invalidEmail
}
}
func validatePassword(_ password: String) throws {
guard password.count >= 8 else {
throw FormError.passwordTooShort
}
}
func validateForm(email: String, password: String) throws {
try validateEmail(email)
try validatePassword(password)
print("Alles in Ordnung ✅")
}
do {
try validateForm(email: "anna@example.com", password: "kurz")
} catch let error as FormError {
print(error.message)
} catch {
print("Unbekannter Fehler.")
}
// Das Passwort braucht mindestens 8 Zeichen.Zusammenfassung
- Fehlertypen sind meist Enums, die das Protokoll
Errorerfüllen. throwsmarkiert eine Funktion, die scheitern kann.throwwirft den Fehler.do { try ... } catch { ... }fängt Fehler ab. Incatchheißt der Fehlererror.- Mehrere
catch-Blöcke behandeln Fehler einzeln. try?macht aus dem Ergebnis ein Optional, der Grund geht verloren.- Funktionen können Fehler weiterreichen, indem sie selbst
throwssind. - Ein Grund → Optional, mehrere Gründe → Fehler.
Spickzettel
enum LoginError: Error { case wrongPassword, noNetwork }
func login(_ pw: String) throws -> String {
guard pw == "geheim" else { throw LoginError.wrongPassword }
return "Token"
}
do {
let token = try login("abc")
} catch LoginError.wrongPassword {
...
} catch {
print(error)
}
let token = try? login("abc") // Optional