Fiche 00.07 — Gestion des erreurs : Error, Result, throws
Objectif
Maîtriser le modèle d'erreurs de Swift moderne : throws/do-catch, les variantes try/try?/try!, transformer une erreur technique en message UI affichable via LocalizedError, et savoir quand choisir async throws plutôt que Result.
1. Le pont avec UIKit / Objective-C
En UIKit/ObjC tu travaillais avec NSError ** error (paramètre out) et tu testais if error != nil. C'était optionnel, on pouvait l'ignorer. En Swift, une fonction marquée throws t'oblige à gérer l'erreur (ou à la propager) : le compilateur refuse de compiler sinon. Et NSError (domain + code) est remplacé par n'importe quel type conforme au protocole Error, le plus souvent une enum.
2. Définir une erreur typée avec une enum
Error est juste un protocole vide (un marqueur). Une enum est le choix idiomatique : cas exhaustifs, valeurs associées pour le contexte.
C'est l'équivalent typé et exhaustif de tes anciens NSError avec des code magiques. Ici le switch sur l'erreur sera vérifié par le compilateur.
3. throw, throws, do-catch
throwssur la signature : « cette fonction peut échouer ».throwdans le corps : on lève l'erreur (arrêt immédiat, comme unreturn).do-catchchez l'appelant : on attrape.
Note : pas de try/catch à la Java où on attrape par hasard. En Swift, catch fait du pattern matching, et le dernier catch sans pattern est obligatoire si tu n'es pas exhaustif.
4. try, try?, try! : les trois variantes
try?: tu transformes l'échec ennil. Pratique quand l'erreur précise ne t'intéresse pas (ex : un cache facultatif).try!: tu garantis que ça ne peut pas échouer ici (ex : une regex littérale valide, une ressource du bundle). En cas d'erreur → crash. À réserver aux invariants, jamais sur du réseau ou des entrées utilisateur.
Anti-pattern : le try? qui avale l'erreur
Règle : try? est acceptable seulement si l'échec a une suite réellement neutre. Dès qu'un humain doit comprendre quelque chose, ne l'avale pas.
5. async throws : la norme moderne
Une fonction asynchrone qui peut échouer combine les deux mots-clés, dans cet ordre : async throws. À l'appel, l'ordre est try await.
Côté appelant (typiquement dans un ViewModel @Observable) :
C'est le couple async throws + do-catch + état .error(String) qui relie cette fiche aux vues d'erreur (cf. fiche 03.05).
6. Mapper une erreur technique en message UI
Ne montre jamais URLError -1009 ou The data couldn't be read à l'utilisateur. Deux approches complémentaires.
a) LocalizedError pour un message « par défaut »
Conformer ton erreur à LocalizedError permet à error.localizedDescription de renvoyer ton texte.
Attention : errorDescription n'est appelé que si l'erreur EST une APIError. Une URLError brute renverra encore son message système.
b) Une fonction de mapping qui couvre aussi les erreurs système
Plus robuste : tu centralises la traduction « erreur quelconque → message actionnable ».
Un bon message UI est actionnable : il dit quoi faire (« réessaie », « reconnecte-toi », « vérifie ton réseau »), pas juste « erreur ».
7. Result<Success, Failure> : encore utile, mais legacy
Result est une enum à deux cas (.success, .failure) qui transporte le résultat OU l'erreur comme une valeur. Avant async/await, c'était LA façon de gérer l'erreur dans un callback (impossible de throw à travers une closure asynchrone).
Tu le croises encore dans : des SDK tiers, du code Combine (Future, sink(receiveCompletion:)), et des bases de code pré-2021.
8. Convertir Result ⇄ async throws
Le pont est utile pour wrapper une vieille API dans du code moderne, ou l'inverse.
withCheckedThrowingContinuation + resume(with:) est le pattern standard pour moderniser une API à completion handler sans la réécrire.
9. Typed throws (Swift 6) — intro courte
Depuis Swift 6, tu peux préciser le type d'erreur jeté : throws(APIError). L'appelant attrape alors une erreur typée (le catch connaît le type exact, plus besoin de cast).
throws « classique » équivaut en fait à throws(any Error), et une fonction non-jetante équivaut à throws(Never) (Never = le type bottom, qui n'a aucune valeur, donc « ne peut rien jeter »). En pratique : utile pour des libs/algorithmes au domaine d'erreur fermé. Pour de l'app classique (réseau, décodage), throws simple reste recommandé — le typage strict devient vite contraignant dès qu'une couche relaie des erreurs hétérogènes.
Points à connaître
try?n'avale pas que l'erreur, il avale l'info de debug. Logge avant de réduire ànil, sinon tu pilotes à l'aveugle en prod.localizedDescription≠ message produit. Sur une erreur non-LocalizedError, c'est le message système (souvent en anglais, technique). Passe toujours par ta fonction de mapping pour l'UI.errorDescriptiondoit retourner uneString?(pasString) : c'est une propriété du protocoleLocalizedError. Oublier le?= ne conforme pas.- Ordre des mots-clés. Signature :
async throws. Appel :try await. Inverser ne compile pas.
Exercice
Crée une enum LoginError: Error, LocalizedError avec au moins 3 cas réalistes (wrongPassword, accountLocked, network). Implémente errorDescription pour que chaque cas renvoie un message clair et actionnable destiné à l'utilisateur (pas un jargon technique). Bonus : écris une fonction func login(email: String, password: String) async throws -> Void qui throw un de ces cas selon des conditions bidon, et appelle-la dans un do-catch qui affecte error.localizedDescription à une variable var uiMessage.
Question d'entretien
« Result ou async throws : lequel choisis-tu en 2026, et pourquoi ? »
async throwspar défaut. C'est la norme depuis Swift Concurrency : code linéaire (pas de pyramide de callbacks), propagation gratuite de l'erreur, et intégration native avecTask/@MainActor. Je réserveResultaux ponts avec du code legacy : SDK à completion handler, Combine, ou quand je veux stocker un succès-ou-échec comme une valeur (ex : dans un cache ou un état). Et je sais convertir l'un en l'autre viawithCheckedThrowingContinuationouresult.get().
« Quelle différence entre try? et try! ? »
try?transforme l'échec ennil(l'erreur est perdue) — pour un échec sans conséquence.try!affirme que l'appel ne peut pas échouer ici et crashe sinon — réservé aux invariants (ressource du bundle, regex littérale), jamais sur du réseau ou une entrée utilisateur.
« À quoi sert LocalizedError ? »
À fournir le texte renvoyé par
localizedDescriptionviaerrorDescription. Ça permet de produire un message lisible par défaut. Mais comme il n'est appelé que sur tes propres types, je double toujours d'une fonction de mapping qui gère aussi lesURLErroret autres erreurs système pour ne jamais afficher de code technique.
Résumé
Errorest un protocole marqueur ; uneenumavec valeurs associées est le choix idiomatique pour une erreur typée.throwsrend la gestion d'erreur obligatoire (contrairement auNSError**ignorable d'UIKit) ;do-catchfait du pattern matching.trypropage,try?réduit ànil(et perd l'erreur),try!crashe —try?silencieux est un anti-pattern dès qu'un humain doit comprendre l'échec.async throws(try await) est la norme moderne ;Resultreste utile pour callbacks legacy/Combine, et on convertit viawithCheckedThrowingContinuation/.get().- Ne jamais montrer une erreur technique (
URLError -1009) à l'utilisateur : mappe vers un message actionnable, viaLocalizedError.errorDescription+ une fonctionmessage(for:)qui couvre aussi les erreurs système. typed throws(Swift 6,throws(MonError)) donne une erreur typée aucatch; utile pour un domaine d'erreur fermé,throwssimple reste le défaut en app.