Swiftlang
Art
Lektion 9 von 12
Niveau
Grundlagen
Lesezeit
etwa 4 Minuten
Stand

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.

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.

throws.swiftAusgeführt Swift 6.4.0, Linux x86-64, 19.10.2026
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 einem AnmeldeFehler.
  • throw .leererName beendet die Funktion sofort mit diesem Fehler. Kein return nötig.
  • Beim Aufruf steht try. Es markiert für Leser jede Stelle, an der der Ablauf abbrechen kann.
  • do umschließt den Code, catch behandelt die Fehler. Weil der Fehlertyp feststeht, kannst du direkt auf die Fälle prüfen: catch .zuKurz(let mindestens).
  • try? macht aus einem Fehler einfach nil. 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:

try-fehlt.swiftKompiliert absichtlich nicht Meldung von Swift 6.4.0, Linux x86-64, 19.10.2026
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:

result.swiftAusgeführt Swift 6.4.0, Linux x86-64, 19.10.2026
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?

SituationNimm
Ein Wert kann fehlen, der Grund ist egal oder offensichtlichOptional (Int("x") liefert nil)
Ein Vorgang kann aus verschiedenen Gründen scheitern, der Aufrufer soll reagierenthrows
Das Ergebnis soll gespeichert, gesammelt oder später ausgewertet werdenResult
Der Fehler ist ein Programmierfehler, der nie passieren darfprecondition 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

  1. Schreibe eine Funktion teile(_ a: Int, durch b: Int) throws(RechenFehler) -> Int, die bei b == 0 einen Fehler .divisionDurchNull wirft. Rufe sie mit 10, 2 und 1, 0 auf und gib jeweils Ergebnis oder Fehlermeldung aus.
  2. Wandle die Funktion aus Übung 1 in eine Variante um, die Result<Int, RechenFehler> zurückgibt, und summiere mit compactMap und try? alle erfolgreichen Ergebnisse von [(10, 2), (3, 0), (9, 3)].
Lösung zu Übung 1
loesung-1.swiftAusgeführt Swift 6.4.0, Linux x86-64, 19.10.2026
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
loesung-2.swiftAusgeführt Swift 6.4.0, Linux x86-64, 19.10.2026
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.