Result Type — un type conteneur qui représente le résultat d’une opération pouvant aboutir avec succès (Success) ou échec (Failure). Contrairement aux exceptions, Result transmet l’erreur comme une valeur normale sans déroulement de pile, ce qui rend la gestion des erreurs attendues plus sûre et composable. Selon Apple Swift Documentation (2026), Result<Success, Failure> en Swift permet d’enchaîner des opérations avec une gestion automatique des erreurs via map et flatMap sans interrompre l’exécution du programme.
Points clés
Result Type est un type générique qui encapsule le résultat d’une opération pouvant réussir ou échouer. Contrairement aux exceptions, où une erreur interrompt le flux normal et nécessite un déroulement de pile pour trouver un bloc catch, Result transmet l’erreur comme une valeur normale — l’appelant reçoit toujours un objet et décide comment le traiter. C’est particulièrement utile pour les erreurs attendues : entrée invalide, règles métier, rejet du serveur, où les exceptions seraient un mécanisme trop lourd.
Le concept de Result provient de la programmation fonctionnelle, où des types similaires sont appelés Either (ou Left/Right). En Swift, Result est devenu un type standard dans Swift 5.0 ; en Kotlin, Result<T> est apparu dans la bibliothèque standard ; en Dart 3.0, Result<T> intégré a été introduit. Chaque implémentation fournit des méthodes pour travailler avec le conteneur : map (transformer la valeur de succès), flatMap (enchaîner des fonctions Result), mapError (transformer l’erreur), fold (traiter les deux cas). Result Type est une pierre angulaire de la gestion fonctionnelle des erreurs dans le développement mobile, une alternative à try-catch pour les scénarios attendus.
L’avantage clé de Result est la composabilité. Vous pouvez combiner plusieurs opérations, chacune pouvant échouer, en une seule chaîne sans blocs try-catch imbriqués. Si une opération dans la chaîne échoue avec Failure, toute la chaîne est interrompue et retourne Failure — sans une seule condition if ou bloc catch. Cela rend le code linéaire et lisible, en particulier dans les scénarios avec plusieurs requêtes API séquentielles ou vérifications de règles métier.
En Swift, Result<Success, Failure> est une énumération avec deux cas : .success(Success) et .failure(Failure), où Failure est contraint par le protocole Error. Le Result de Swift est un type entièrement fonctionnel avec les méthodes map, flatMap, mapError et get. get est une méthode spéciale : elle retourne la valeur Success si le résultat est réussi et lance l’erreur si c’est Failure. Cela permet à Result d’agir comme un pont entre les styles fonctionnel et basé sur les exceptions : traiter via map/flatMap dans une chaîne, et à la fin utiliser get avec do-catch pour s’intégrer avec du code throws.
Le Result de Swift est une énumération avec des paramètres génériques, ce qui permet au compilateur de vérifier le traitement exhaustif via switch ou do-catch. Si vous ajoutez un nouveau cas à l’énumération NetworkError, le compilateur générera une erreur dans toutes les expressions switch où ce cas n’est pas traité. La vérification exhaustive est le principal avantage de Result par rapport aux exceptions : le compilateur garantit que toutes les erreurs possibles sont prises en compte au moment de la compilation. En revanche, les exceptions ne sont pas vérifiées par le compilateur en Swift (seulement throws est déclaré, mais pas le type d’erreur).
enum NetworkError: Error {
case badURL
case requestFailed(String)
case decodingFailed
}
func fetchUser(id: Int) -> Result<User, NetworkError> {
guard let url = URL(string: "https://api.example.com/users/\(id)") else {
return .failure(.badURL)
}
let result = performRequest(url: url)
switch result {
case let .success(data):
if let user = try? JSONDecoder().decode(User.self, from: data) {
return .success(user)
}
return .failure(.decodingFailed)
case let .failure(error):
return .failure(.requestFailed(error.localizedDescription))
}
}
// Utilisation avec switch
let result = fetchUser(id: 42)
switch result {
case .success(let user):
showUser(user)
case .failure(.badURL):
logError("Invalid URL")
case .failure(.requestFailed(let msg)):
showAlert(msg)
case .failure(.decodingFailed):
logError("Decoding error")
}
Result<Success, Failure> permet de typer l’erreur au niveau du type : la fonction fetchUser retourne Result<User, NetworkError>, où NetworkError est une énumération concrète avec trois cas. L’appelant traite chaque cas via switch avec une couverture exhaustive — le compilateur vérifie que toutes les variantes sont traitées. Le switch exhaustif est un avantage clé de Result par rapport aux exceptions : le compilateur garantit que vous n’oublierez pas de traiter .badURL, .requestFailed ou .decodingFailed. Avec les exceptions, le compilateur n’exige pas de traitement, et un catch(.badURL) manquant peut facilement passer inaperçu lors de la révision de code.
En Kotlin, Result<T> est un type intégré de la bibliothèque standard qui représente un succès (T) ou une erreur (Throwable). Contrairement à Swift, le Result de Kotlin ne permet pas de spécifier un type d’erreur concret — seulement Throwable. Cela est fait pour la simplicité mais nécessite une vérification supplémentaire du type d’erreur via l’expression when. Le Result de Kotlin prend en charge fold (traiter les deux cas), getOrNull (succès ou null), getOrDefault (succès ou valeur par défaut), map, recover, andThen. Une particularité de Kotlin : Result n’est pas conçu pour la propagation directe à travers les limites de fonctions — il ne peut pas être utilisé comme type de retour pour les méthodes Android SDK ou l’API Kotlin Coroutines sans adaptation supplémentaire.
fun parseJson(input: String): Result<JsonObject> {
return runCatching {
JsonParser.parseString(input).asJsonObject()
}
}
fun validateEmail(email: String): Result<String> {
return if (email.contains("@")) {
Result.success(email.trim())
} else {
Result.failure(IllegalArgumentException("Invalid email"))
}
}
data class SignupData(val name: String, val email: String)
fun processSignup(name: String, email: String): SignupResult {
return validateEmail(email).fold(
onSuccess = { validateName(name) },
onFailure = { SignupResult.Error("Invalid email") }
)
}
runCatching est un wrapper pratique en Kotlin qui attrape toute exception et retourne Result.failure. validateEmail retourne Result.success ou Result.failure selon la validation. fold traite les deux cas de manière compacte : en cas de Success, la fonction de validation suivante est appelée ; en cas de Failure, SignupResult.Error est retourné. Important : le Result de Kotlin n’est pas conçu pour le stockage dans des champs de data class ou la transmission directe à travers les limites de fonctions suspend — utilisez des classes sealed personnalisées (Success/Error/Loading) pour représenter les états UI dans Jetpack Compose ou MVVM.
Depuis Dart 3.0, la bibliothèque standard inclut un Result<T> intégré — une classe sealed avec deux constructeurs : T.ok() (succès) et Error.error() (erreur avec Object et StackTrace). Avant Dart 3.0, les développeurs Flutter utilisaient Either<L, R> du package dartz ou des classes sealed personnalisées. Le Result intégré dans Dart est minimaliste : il ne fournit pas map/flatMap directement — ces fonctions doivent être implémentées via when ou en utilisant des méthodes d’extension. Pour un traitement fonctionnel sérieux, Either de fpdart reste une solution plus puissante avec le support de map, flatMap, mapLeft, fold, andThen et des opérateurs bind.
Either<L, R> est un type biaisé à gauche du package fpdart, où Left est l’erreur et Right le succès. Contrairement au Result<T> intégré, Either type l’erreur au niveau du paramètre de type (L), permettant la différenciation du type d’erreur à la compilation. Le package fpdart fournit un ensemble complet de combinateurs fonctionnels : map (Right -> Right), mapLeft (Left -> Left), flatMap (bind — Either imbriqué), andThen (enchaînement sans transformation), fold (sortie de Either), getOrElse (valeur par défaut). Pour les applications Flutter avec une approche fonctionnelle, Either est le standard de facto.
import 'dart:convert';
import 'package:fpdart/fpdart.dart';
class UserService {
Either<AppError, User> fetchUser(String id) {
try {
final response = await http.get(
Uri.parse('https://api.example.com/users/$id')
);
if (response.statusCode == 200) {
final user = User.fromJson(
json.decode(response.body)
);
return Either.of(user);
}
return Either.left(
AppError.serverError(response.statusCode)
);
} on SocketException catch (e) {
return Either.left(AppError.networkError(e.message));
}
}
}
// Utilisation avec fold
final result = await service.fetchUser('42');
result.fold(
(left) => showError(left.message),
(right) => showUser(right),
);
Either<L, R> de fpdart est un type biaisé à gauche : Left — erreur, Right — succès. fetchUser retourne Either<AppError, User>, où AppError est une classe sealed avec des types d’erreur concrets (serverError, networkError). fold traite les deux cas : le premier callback pour Left (erreur), le second pour Right (succès). Dans Dart 3.0, le Result intégré prend également en charge fold, mais ne fournit pas map/flatMap. Pour les chaînes, Either de fpdart fournit map, flatMap (bind), mapLeft, andThen — un ensemble complet de combinateurs fonctionnels pour la composition d’erreurs.
Le principal avantage de Result par rapport aux exceptions est la composition. Si vous avez plusieurs opérations, chacune pouvant échouer, vous pouvez les enchaîner en utilisant map et flatMap sans un seul if imbriqué ou try-catch. map transforme la valeur de succès : Result.success(x) -> Result.success(f(x)). flatMap (également appelé bind ou andThen) est pour les cas où la transformation elle-même retourne un Result : Result.success(x) -> f(x) -> Result<Y>. Si une étape échoue avec Failure, les opérations suivantes ne sont pas exécutées — la chaîne est interrompue.
data class UserRequest(val userId: String, val token: String)
sealed class AuthError {
data object InvalidToken : AuthError()
data class UserNotFound(val id: String) : AuthError()
}
typealias Outcome<T> = Either<AuthError, T>
fun validateToken(token: String): Outcome<String> =
if (token.isNotBlank()) Either.right(token)
else Either.left(AuthError.InvalidToken)
fun fetchProfile(userId: String): Outcome<Profile> =
if (userId == "42") Either.right(Profile("Alice"))
else Either.left(AuthError.UserNotFound(userId))
// Composition via flatMap (andThen dans fpdart)
val result = validateToken("abc123")
.flatMap { fetchProfile("42") }
.map { it.name }
.getOrElse { "Guest" }
println(result) // "Alice"
Chaîne : validateToken -> fetchProfile -> map name -> getOrElse « Invité ». Si validateToken retourne Left (InvalidToken), la chaîne est interrompue et retourne « Invité ». Si fetchProfile retourne Left (UserNotFound) — également « Invité ». Si les deux opérations réussissent — le nom du profil. flatMap permet de combiner des fonctions Either, chacune pouvant échouer, en une seule chaîne linéaire. Dans le style traditionnel d’exceptions, le même code nécessiterait deux try-catch imbriqués ou des vérifications null. getOrElse à la fin est le point de sortie de la composition, fournissant une valeur par défaut pour le cas Failure.
Result et exceptions ne sont pas des approches mutuellement exclusives. Chacune a son propre domaine, et dans une application mobile bien conçue, les deux sont utilisés. Le choix dépend du fait que l’erreur soit attendue ou inattendue. Result est pour les erreurs attendues qui font partie de la logique métier : e-mail invalide, fonds insuffisants, limite de taux dépassée. Exceptions sont pour les erreurs système inattendues : perte de réseau, OutOfMemoryError, NullPointerException (qui ne devraient pas se produire mais se produisent).
| Critère | Result Type | Exceptions (Exception/Error) |
|---|---|---|
| Type d’erreur | Attendues (logique métier) | Inattendues (système) |
| Performance | Coût faible (sans déroulement de pile) | Coût élevé (déroulement de pile, capture de StackTrace) |
| Composition | Via map/flatMap — chaînes linéaires | Try-catch imbriqués — difficiles à lire |
| Compilateur | Vérification exhaustive (switch/when) | Exceptions vérifiées seulement en Java |
| Flux d’exécution | Non interrompu — erreur comme valeur | Interrompu jusqu’au catch le plus proche |
| Test | Facile : vérifier le résultat, assert isSuccess/isError | Nécessite assertThrows et objets mock |
| Quand utiliser | Validation métier, chaînes de requêtes, formulaires | Perte de réseau, erreurs E/S, pannes système |
Règle pratique : si l’erreur fait partie du flux de travail normal de l’application (utilisateur a saisi un e-mail invalide, permissions insuffisantes) — utilisez Result. Si l’erreur est une situation exceptionnelle (serveur ne répond pas, mémoire épuisée) — utilisez les exceptions. Dans le développement mobile, Result aux limites des couches (UseCase -> ViewModel) et les exceptions à l’intérieur des couches (API -> Repository) est un motif courant qui combine les avantages des deux approches.
La migration du code existant basé sur les exceptions vers Result doit être progressive. Commencez par les limites des couches : enveloppez les appels de fonctions throws dans Result { try ... } (Swift) ou runCatching { ... } (Kotlin). Remplacez ensuite le type de retour des méthodes Repository et UseCase par Result/Either, en laissant l’implémentation interne sur les exceptions. Dans la dernière étape, migrez le ViewModel : au lieu de UiState avec exceptions, utilisez une classe sealed UiState<T> (Loading, Success, Error), où Error stocke une erreur de domaine, pas un Throwable. La migration progressive permet de tester chaque couche séparément sans refactorisation globale.
Questions fréquentes
Optional (T?) représente la présence ou l’absence d’une valeur — nil signifie « pas de données » mais n’explique pas pourquoi. Result (Success/Failure) contient non seulement le succès mais aussi la raison de l’erreur avec un type concret. Utilisez Optional quand l’absence est normale (par exemple, un champ de profil optionnel), et Result quand vous avez besoin d’informations sur l’erreur.
En Swift, utilisez Result { try throwingFunc() } — le constructeur Result prend une fermeture throws. En Kotlin, utilisez runCatching { throwingFunc() }, qui retourne Result<T>. En Dart, utilisez Result<T>.tryCatch(() => throwingFunc()). Cela permet une intégration facile du code basé sur les exceptions dans les chaînes Result.
Utilisez mapError (Swift) ou mapLeft (Either dans Dart/Kotlin) pour transformer le type d’erreur sans changer la valeur de succès. Si vous devez traiter les deux cas et retourner une valeur unique, utilisez fold. Pour la journalisation sans interrompre la chaîne, utilisez onFailure (Kotlin) ou un point d’inspection de préfixe.
Oui, mais avec précaution. Le Result<T> de Kotlin n’est pas recommandé comme type de retour pour les fonctions suspend directement en raison de particularités du compilateur K2 et de la réflexion. Utilisez votre propre classe sealed NetworkResult<T> (Success, Error, Loading) pour représenter les états dans les coroutines. Pour les erreurs attendues dans la logique métier, Either d’Arrow est une alternative plus puissante.
fold est une méthode qui prend deux callbacks : onSuccess (pour le cas de succès) et onFailure (pour le cas d’erreur), et retourne une valeur unique de n’importe quel type. C’est l’équivalent d’une expression switch mais sous forme de fonction d’ordre supérieur. fold est le principal point de sortie des chaînes Result, où vous convertissez Success/Failure en UiState, une chaîne pour l’utilisateur ou un autre Result.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi