Lektion 1.17 · Stufe 1 – Swift-Fundament

Fehlerbehandlung

ca. 15 Minuten Lesezeit

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:

Swift
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:

Swift
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
}
  • throws steht nach den Parametern und vor dem Pfeil ->.
  • throw beendet die Funktion sofort, ähnlich wie return, liefert aber einen Fehler statt eines Ergebnisses.

Statt if kannst du natürlich auch guard verwenden. Das ist hier sogar besonders elegant:

Swift
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:

Swift
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:

Swift
do {
    let name = try validate(username: "Al")
    print("Willkommen, \(name)!")
} catch {
    print("Fehler: \(error)")
}
// Fehler: tooShort

So liest du das:

  • do { ... }: „Versuch Folgendes.“
  • try: markiert die Stelle, an der ein Fehler auftreten kann. Tritt einer auf, springt Swift sofort in den catch-Block. Die restlichen Zeilen im do-Block werden übersprungen.
  • catch { ... }: „Falls es schiefgeht, mach das.“ Innerhalb von catch steht der Fehler automatisch in einer Konstante namens error.

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:

Swift
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:

Swift
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:

Swift
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:

Swift
let name = try? validate(username: "Al")
print(name)   // nil

try? 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:

Swift
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:

Swift
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?

SituationWerkzeug
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önnenFehler (throws)
Der Grund soll dem Nutzer angezeigt werdenFehler mit verständlicher Meldung
Etwas, das grundsätzlich schiefgehen kann: Netzwerk, Dateien, DatenbankenFehler. 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

Swift
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

Swift
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

Swift
enum UsernameError {
    case tooShort
}
throw UsernameError.tooShort

Fehlermeldung

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

Anfänger

Ü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!
Swift
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)")
    }
}
Fortgeschritten

Ü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!
Swift
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 €.
Profi

Ü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!
Swift
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 Error erfüllen.
  • throws markiert eine Funktion, die scheitern kann. throw wirft den Fehler.
  • do { try ... } catch { ... } fängt Fehler ab. In catch heißt der Fehler error.
  • 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 throws sind.
  • Ein Grund → Optional, mehrere Gründe → Fehler.

Spickzettel

Swift
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

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.