Swift 6 hat endlich eines der meistgewünschten Features in Swift eingeführt: Typed Throws. Diese Verbesserung erlaubt es dir, genau anzugeben, welche Fehlertypen eine Funktion werfen kann, und bringt damit Swifts Typsicherheit in die Fehlerbehandlung. Aber mit dieser Mächtigkeit kommt eine neue Herausforderung, die ich “Verschachtelungshölle” nennen würde – ein Problem, das beeinflusst, wie Fehler sich durch die Schichten deiner Anwendung verbreiten.
In diesem Beitrag erkläre ich das Verschachtelungsproblem und zeige dir, wie ich es in ErrorKit mit einem einfachen Protocol gelöst habe, das Typed Throws ohne Boilerplate praxistauglich macht. Als Bonus siehst du, wie richtige Fehlerketten dein Debugging-Erlebnis dramatisch verbessern können.
Typed Throws: Das Versprechen und das Problem
Schauen wir uns zuerst an, was uns Typed Throws in Swift 6 bietet:
// Statt nur 'throws' können wir den Fehlertyp angeben
func processFile() throws(FileError) {
if !fileExists {
throw FileError.fileNotFound(fileName: "config.json")
}
// Implementierung...
}Das ermöglicht eine bessere Fehlerbehandlung an der Aufrufstelle:
do {
try processFile()
} catch FileError.fileNotFound(let fileName) {
print("Could not find file: \(fileName)")
} catch FileError.readFailed {
print("Could not read file")
}
// Kein generischer catch nötig, wenn wir alle möglichen FileError-Fälle behandelt haben!Die Vorteile liegen auf der Hand:
Compile-time-Verifizierung der Fehlerbehandlung
Kein Type-Casting mit
as?in Catch-Blöcken nötigSelbstdokumentierende API, die Aufrufern genau sagt, was schiefgehen kann
IDE-Autovervollständigung für Fehlerfälle
Das Verschachtelungshölle-Problem
Das Problem entsteht bei der Arbeit mit mehrschichtigen Anwendungen. Sieh dir das an:
// Datenbankschicht wirft DatabaseError
func fetchUser(id: String) throws(DatabaseError) {
// Datenbankoperationen...
}
// Profilschicht muss die Datenbankschicht aufrufen
func loadUserProfile(id: String) throws(ProfileError) {
do {
// Problem: Dies wirft DatabaseError, nicht ProfileError
let user = try fetchUser(id: id)
} catch {
// Manuelle Fehlerkonvertierung nötig
switch error {
case DatabaseError.recordNotFound:
throw ProfileError.userNotFound
default:
throw ProfileError.databaseError(error) // Ein Wrapper-Case wird benötigt
}
}
}Das erzeugt mehrere Probleme:
Explosion von Wrapper-Cases: Jeder Fehlertyp braucht Wrapper-Cases für alle möglichen Kind-Fehler
Manuelles Error-Mapping: Repetitive Do-Catch-Blöcke mit expliziter Fehlerkonvertierung
Typen-Inflation: Fehlertypen wachsen mit jeder Schicht und werden schwerer zu pflegen
Verlorener Kontext: Details über den ursprünglichen Fehler gehen bei der Übersetzung oft verloren
Für kleine Apps mag das handhabbar sein. Für größere Apps mit vielen Schichten wird es schnell zu dem, was man als “Verschachtelungshölle” bezeichnen kann.
Die Lösung: Das Catching-Protocol
ErrorKit löst das mit einem einfachen Protocol namens Catching:
public protocol Catching {
static func caught(_ error: Error) -> Self
}Dieses Protocol erfordert einen einzelnen Enum-Case namens caught, der jeden Fehler in deinen Typ einwickelt. So verwendest du es:
enum ProfileError: Throwable, Catching {
case userNotFound
case invalidProfile
case caught(Error) // Ein einziger Case für alle anderen Fehler
var userFriendlyMessage: String {
switch self {
case .userNotFound:
return "User not found."
case .invalidProfile:
return "Profile data is invalid."
case .caught(let error):
// Die Meldung des eingewickelten Fehlers verwenden
return ErrorKit.userFriendlyMessage(for: error)
}
}
}Beachte, dass Throwable ein Drop-in-Ersatz für Error ist (siehe vorheriger Beitrag).
Jetzt passiert die Magie mit der catch-Funktion, die mit dem Protocol mitgeliefert wird:
func loadUserProfile(id: String) throws(ProfileError) {
// Bei bekannten Fehlern direkt werfen
guard isValidID(id) else {
throw ProfileError.invalidInput
}
// Für Operationen, die andere Fehlertypen werfen können, die catch-Funktion verwenden
let user = try ProfileError.catch {
// Jeder hier geworfene Fehler wird automatisch
// in ProfileError.caught(error) eingewickelt
return try fetchUser(id: id)
}
// Rest der Implementierung...
}Beachte, dass die catch-Funktion zurückgibt, was du in der Closure zurückgibst.
Die catch-Funktion wickelt automatisch alle Fehler, die in ihrer Closure geworfen werden, in deinen Fehlertyp ein. Keine manuellen Do-Catch-Blöcke, kein explizites Error-Mapping – es funktioniert einfach. Sogar mehrere try-Ausdrücke sind möglich.
Das Geheimrezept der catch-Funktion
Die catch-Funktion ist elegant einfach:
extension Catching {
public static func `catch`<ReturnType>(
_ operation: () throws -> ReturnType
) throws(Self) -> ReturnType {
do {
return try operation()
} catch {
throw Self.caught(error)
}
}
}Diese Funktion:
Nimmt eine werfende Closure entgegen
Versucht, sie auszuführen
Gibt das Ergebnis bei Erfolg zurück
Wickelt jeden geworfenen Fehler automatisch über deinen
caught-Case einBehält den Rückgabetyp der Operation bei
Das Beste daran? Es funktioniert nahtlos mit Swift 6’s Typed Throws, erhält die Typsicherheit und eliminiert gleichzeitig Boilerplate.
Die Fehlerkette für Debugging erhalten
Einer der größten Vorteile dieses Ansatzes ist, dass er die vollständige Fehlerkette erhält. Statt Kontext zu verlieren, wenn Fehler Schichtgrenzen überschreiten, fügt jede Schicht Informationen hinzu und behält den ursprünglichen Fehler intakt.
ErrorKit nutzt das für mächtiges Debugging mit der errorChainDescription(for:)-Funktion:
do {
try await updateUserProfile()
} catch {
print(ErrorKit.errorChainDescription(for: error))
// Ausgabe zeigt die vollständige Kette:
// AppError
// └─ ProfileError
// └─ DatabaseError
// └─ FileError.notFound(path: "/Users/data.db")
// └─ userFriendlyMessage: "Could not find database file."
}Diese hierarchische Ansicht zeigt dir:
Wo der Fehler entstanden ist (FileError)
Den genauen Weg durch deine Anwendung (FileError -> DatabaseError -> ProfileError -> AppError)
Die konkreten Details, was schiefgelaufen ist (Datei nicht gefunden, mit dem Pfad)
Die benutzerfreundliche Meldung, die dem Nutzer angezeigt würde
Dieses Maß an Einblick ist beim Debugging unbezahlbar, besonders bei komplexen Anwendungen, in denen Fehler tief im Call Stack entstehen können.
Strukturierte Fehlerketten-Ausgabe
Die Fehlerketten-Beschreibung funktioniert durch rekursives Inspizieren der Fehlerstruktur:
static func errorChainDescription(for error: Error) -> String {
// Rekursive Implementierung, die eine hierarchische Beschreibung aufbaut
Self.chainDescription(for: error, enclosingType: type(of: error))
}Siehe hier für die vollständige Implementierung von chainDescription in ErrorKit.
Die Funktion nutzt Swifts Reflection-Fähigkeiten, um:
Den Fehler über die Mirror-API zu inspizieren
Bei Fehlern, die
Catchingkonformieren, den eingewickelten Fehler zu extrahierenBei Enum-Fehlern Case-Namen und zugehörige Werte zu erfassen
Bei Struct- oder Class-Fehlern Typ-Metadaten einzubeziehen
Alles in einer hierarchischen Baumstruktur zu formatieren
Das liefert weit mehr Informationen als Standard-Error-Logging, besonders bei komplexen Fehlerhierarchien.
Eingebaute Unterstützung in ErrorKit
Alle eingebauten Fehlertypen von ErrorKit (wie FileError oder NetworkError) konformieren bereits zu Catching, sodass du sie sofort verwenden kannst:
func saveUserData() throws(DatabaseError) {
// Wickelt automatisch SQLite-Fehler, Dateisystem-Fehler usw. ein
try DatabaseError.catch {
try database.beginTransaction()
try database.execute(query)
try database.commit()
}
}Praxisbeispiel: Eine typische Anwendung
Schauen wir uns an, wie das in einem vollständigeren Beispiel funktioniert:
// Datenzugriffsschicht
func fetchUserData(id: String) throws(DatabaseError) {
guard database.isConnected else {
throw DatabaseError.connectionFailed
}
// Hier könnten Dateisystem-Fehler auftreten
try DatabaseError.catch {
let query = try QueryBuilder.build(for: id)
return try database.execute(query)
}
}
// Geschäftslogik-Schicht
func processUserProfile(id: String) throws(ProfileError) {
guard isValidID(id) else {
throw ProfileError.invalidInput
}
// Wickelt automatisch DatabaseError ein
let userData = try ProfileError.catch {
return try fetchUserData(id: id)
}
// Nutzerdaten verarbeiten...
}
// Präsentationsschicht
func displayUserProfile(id: String) throws(UIError) {
// Wickelt automatisch ProfileError ein (das möglicherweise DatabaseError enthält)
let profile = try UIError.catch {
return try processUserProfile(id: id)
}
// Profil anzeigen...
}Wenn eine Datenbankverbindung fehlschlägt, siehst du Folgendes in der Fehlerkette:
UIError
└─ ProfileError
└─ DatabaseError.connectionFailed
└─ userFriendlyMessage: "Unable to establish a connection to the database. Check your network settings and try again."Das zeigt dir genau, was passiert ist und wo der Fehler entstanden ist, was das Debugging erheblich erleichtert. Der zusätzliche Kontext kann dir den entscheidenden Hinweis geben, um das Problem zu beheben!
Fazit
Swift 6’s Typed Throws ist eine mächtige Ergänzung der Sprache, bringt aber Herausforderungen bei der Fehlerweiterleitung über Schichten hinweg mit sich. Das Catching-Protocol bietet eine einfache, elegante Lösung, die Typsicherheit erhält und gleichzeitig Boilerplate eliminiert.
Kombiniert mit ErrorKits errorChainDescription-Funktion wird Fehlerbehandlung zu einem mächtigen Debugging-Werkzeug. Nutze ErrorKit jetzt und profitiere von vielen weiteren Verbesserungen, die Fehlerbehandlung in Swift in realen Apps nützlicher machen:
Hast du schon angefangen, Swift 6’s Typed Throws zu nutzen? Wie gehst du mit der Fehlerweiterleitung über Schichten in deinen Apps um? Schreib mir auf den sozialen Kanälen (Links unten)!

