Result Type — un tipo contenitore che rappresenta il risultato di un'operazione che può completarsi con successo (Success) o errore (Failure). A differenza delle eccezioni, Result passa l'errore come un valore normale senza srotolamento dello stack, rendendo la gestione degli errori previsti più sicura e componibile. Secondo Apple Swift Documentation (2026), Result<Success, Failure> in Swift consente di concatenare operazioni con gestione automatica degli errori tramite map e flatMap senza interrompere l'esecuzione del programma.
Punti chiave
Result Type è un tipo generico che incapsula il risultato di un'operazione che può avere successo o fallire. A differenza delle eccezioni, dove un errore interrompe il flusso normale e richiede lo srotolamento dello stack per trovare un blocco catch, Result passa l'errore come un valore normale — il chiamante riceve sempre un oggetto e decide come gestirlo. Questo è particolarmente utile per gli errori previsti: input non valido, regole di business, rifiuto del server, dove le eccezioni sarebbero un meccanismo troppo pesante.
Il concetto di Result proviene dalla programmazione funzionale, dove tipi simili sono chiamati Either (o Left/Right). In Swift, Result è diventato un tipo standard in Swift 5.0; in Kotlin, Result<T> è apparso nella libreria standard; in Dart 3.0, è stato introdotto Result<T> integrato. Ogni implementazione fornisce metodi per lavorare con il contenitore: map (trasformare il valore di successo), flatMap (concatenare funzioni Result), mapError (trasformare l'errore), fold (gestire entrambi i casi). Result Type è una pietra miliare della gestione funzionale degli errori nello sviluppo mobile, un'alternativa a try-catch per scenari previsti.
Il vantaggio principale di Result è la componibilità. Puoi combinare più operazioni, ciascuna delle quali può fallire, in un'unica catena senza blocchi try-catch annidati. Se un'operazione nella catena fallisce con Failure, l'intera catena viene interrotta e restituisce Failure — senza una singola condizione if o blocco catch. Questo rende il codice lineare e leggibile, specialmente in scenari con più richieste API sequenziali o controlli di regole di business.
In Swift, Result<Success, Failure> è un enum con due casi: .success(Success) e .failure(Failure), dove Failure è vincolato dal protocollo Error. Result in Swift è un tipo completamente funzionale con i metodi map, flatMap, mapError e get. get è un metodo speciale: restituisce il valore di Success se il risultato è positivo e lancia l'errore se è Failure. Ciò consente a Result di fungere da ponte tra gli stili funzionale e basato sulle eccezioni: gestire tramite map/flatMap in una catena e alla fine usare get con do-catch per integrarsi con codice throws.
Result in Swift è un enum con parametri generici, che consente al compilatore di verificare la gestione esaustiva tramite switch o do-catch. Se aggiungi un nuovo caso all'enum NetworkError, il compilatore produrrà un errore in tutte le espressioni switch in cui quel caso non viene gestito. Il controllo esaustivo è il principale vantaggio di Result rispetto alle eccezioni: il compilatore garantisce che tutti gli errori possibili siano considerati in fase di compilazione. Al contrario, le eccezioni non vengono verificate dal compilatore in Swift (solo throws viene dichiarato, ma non il tipo di errore).
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))
}
}
// Utilizzo con 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> consente di tipizzare l'errore a livello di tipo: la funzione fetchUser restituisce Result<User, NetworkError>, dove NetworkError è un enum concreto con tre casi. Il chiamante gestisce ogni caso tramite switch con copertura esaustiva — il compilatore verifica che tutte le varianti siano gestite. Lo switch esaustivo è un vantaggio chiave di Result rispetto alle eccezioni: il compilatore garantisce che non dimenticherai di gestire .badURL, .requestFailed o .decodingFailed. Con le eccezioni, il compilatore non richiede gestione e un catch(.badURL) mancante può facilmente passare inosservato nella revisione del codice.
In Kotlin, Result<T> è un tipo integrato della libreria standard che rappresenta successo (T) o errore (Throwable). A differenza di Swift, Result in Kotlin non consente di specificare un tipo di errore concreto — solo Throwable. Ciò è fatto per semplicità ma richiede un controllo aggiuntivo del tipo di errore tramite l'espressione when. Result in Kotlin supporta le funzioni fold (gestire entrambi i casi), getOrNull (successo o null), getOrDefault (successo o valore predefinito), map, recover, andThen. Una particolarità di Kotlin: Result non è progettato per la propagazione diretta attraverso i confini delle funzioni — non può essere usato come tipo di ritorno per metodi Android SDK o API Kotlin Coroutines senza adattamento aggiuntivo.
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 è un wrapper conveniente in Kotlin che cattura qualsiasi eccezione e restituisce Result.failure. validateEmail restituisce Result.success o Result.failure in base alla validazione. fold gestisce entrambi i casi in modo compatto: in caso di Success, viene chiamata la funzione di validazione successiva; in caso di Failure, viene restituito SignupResult.Error. Importante: Result in Kotlin non è progettato per l'archiviazione in campi di data class o la trasmissione diretta attraverso i confini di funzioni suspend — usa classi sealed personalizzate (Success/Error/Loading) per rappresentare gli stati UI in Jetpack Compose o MVVM.
Da Dart 3.0, la libreria standard include un Result<T> integrato — una classe sealed con due costruttori: T.ok() (successo) e Error.error() (errore con Object e StackTrace). Prima di Dart 3.0, gli sviluppatori Flutter usavano Either<L, R> dal pacchetto dartz o classi sealed personalizzate. Il Result integrato in Dart è minimalista: non fornisce map/flatMap direttamente — queste funzioni devono essere implementate tramite when o usando metodi di estensione. Per una gestione funzionale seria, Either di fpdart rimane una soluzione più potente con supporto per map, flatMap, mapLeft, fold, andThen e operatori bind.
Either<L, R> è un tipo biased a sinistra del pacchetto fpdart, dove Left è l'errore e Right è il successo. A differenza di Result<T> integrato, Either tipizza l'errore a livello di parametro di tipo (L), consentendo la differenziazione del tipo di errore in fase di compilazione. Il pacchetto fpdart fornisce un set completo di combinatori funzionali: map (Right -> Right), mapLeft (Left -> Left), flatMap (bind — Either annidato), andThen (concatenazione senza trasformazione), fold (uscita da Either), getOrElse (valore predefinito). Per applicazioni Flutter con approccio funzionale, Either è lo 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));
}
}
}
// Utilizzo con fold
final result = await service.fetchUser('42');
result.fold(
(left) => showError(left.message),
(right) => showUser(right),
);
Either<L, R> di fpdart è un tipo biased a sinistra: Left — errore, Right — successo. fetchUser restituisce Either<AppError, User>, dove AppError è una classe sealed con tipi di errore concreti (serverError, networkError). fold gestisce entrambi i casi: il primo callback per Left (errore), il secondo per Right (successo). In Dart 3.0, anche Result integrato supporta fold, ma non fornisce map/flatMap. Per le catene, Either di fpdart fornisce map, flatMap (bind), mapLeft, andThen — un set completo di combinatori funzionali per la composizione degli errori.
Il principale vantaggio di Result rispetto alle eccezioni è la composizione. Se hai più operazioni, ciascuna delle quali può fallire, puoi concatenarle usando map e flatMap senza un singolo if annidato o try-catch. map trasforma il valore di successo: Result.success(x) -> Result.success(f(x)). flatMap (chiamato anche bind o andThen) è per i casi in cui la trasformazione stessa restituisce un Result: Result.success(x) -> f(x) -> Result<Y>. Se un passaggio fallisce con Failure, le operazioni successive non vengono eseguite — la catena viene interrotta.
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))
// Composizione tramite flatMap (andThen in fpdart)
val result = validateToken("abc123")
.flatMap { fetchProfile("42") }
.map { it.name }
.getOrElse { "Guest" }
println(result) // "Alice"
Catena: validateToken -> fetchProfile -> map name -> getOrElse “Ospite”. Se validateToken restituisce Left (InvalidToken), la catena viene interrotta e restituisce “Ospite”. Se fetchProfile restituisce Left (UserNotFound) — anch'“Ospite”. Se entrambe le operazioni hanno successo — il nome del profilo. flatMap consente di combinare funzioni Either, ciascuna delle quali può fallire, in un'unica catena lineare. Nello stile tradizionale delle eccezioni, lo stesso codice richiederebbe due try-catch annidati o controlli null. getOrElse alla fine è il punto di uscita dalla composizione, che fornisce un valore predefinito per il caso Failure.
Result ed eccezioni non sono approcci mutuamente esclusivi. Ciascuno ha il proprio dominio e in un'applicazione mobile ben progettata vengono usati entrambi. La scelta dipende dal fatto che l'errore sia previsto o imprevisto. Result è per errori previsti che fanno parte della logica di business: email non valida, fondi insufficienti, limite di velocità superato. Eccezioni sono per errori di sistema imprevisti: perdita di rete, OutOfMemoryError, NullPointerException (che non dovrebbero accadere ma accadono).
| Criterio | Result Type | Eccezioni (Exception/Error) |
|---|---|---|
| Tipo di errore | Previsti (logica di business) | Imprevisti (sistema) |
| Prestazioni | Costo basso (senza srotolamento stack) | Costo alto (srotolamento stack, cattura StackTrace) |
| Composizione | Tramite map/flatMap — catene lineari | Try-catch annidati — difficili da leggere |
| Compilatore | Controllo esaustivo (switch/when) | Solo eccezioni verificate in Java |
| Flusso di esecuzione | Non interrotto — errore come valore | Interrotto fino al catch più vicino |
| Test | Facile: verificare risultato, assert isSuccess/isError | Richiede assertThrows e oggetti mock |
| Quando usare | Validazione business, catene di richieste, moduli | Perdita di rete, errori I/O, crash di sistema |
Regola pratica: se l'errore fa parte del normale flusso di lavoro dell'applicazione (l'utente ha inserito un'email non valida, permessi insufficienti) — usa Result. Se l'errore è una situazione eccezionale (server non risponde, memoria esaurita) — usa le eccezioni. Nello sviluppo mobile, Result ai confini dei livelli (UseCase -> ViewModel) ed eccezioni all'interno dei livelli (API -> Repository) è un pattern comune che combina i vantaggi di entrambi gli approcci.
La migrazione del codice esistente basato sulle eccezioni a Result dovrebbe essere graduale. Inizia con i confini dei livelli: avvolgi le chiamate a funzioni throws in Result { try ... } (Swift) o runCatching { ... } (Kotlin). Quindi sostituisci il tipo di ritorno dei metodi di Repository e UseCase con Result/Either, mantenendo l'implementazione interna sulle eccezioni. Nell'ultimo passaggio, migra il ViewModel: invece di UiState con eccezioni, usa una classe sealed UiState<T> (Loading, Success, Error), dove Error memorizza un errore di dominio, non un Throwable. La migrazione graduale consente di testare ogni livello separatamente senza una refactoring globale.
Domande frequenti
Optional (T?) rappresenta la presenza o l'assenza di un valore — nil significa “nessun dato” ma non spiega il perché. Result (Success/Failure) contiene non solo il successo ma anche la ragione dell'errore con un tipo concreto. Usa Optional quando l'assenza è normale (ad esempio, un campo profilo opzionale) e Result quando hai bisogno di informazioni sull'errore.
In Swift, usa Result { try throwingFunc() } — il costruttore Result accetta una closure throws. In Kotlin, usa runCatching { throwingFunc() }, che restituisce Result<T>. In Dart, usa Result<T>.tryCatch(() => throwingFunc()). Ciò consente una facile integrazione del codice basato sulle eccezioni nelle catene Result.
Usa mapError (Swift) o mapLeft (Either in Dart/Kotlin) per trasformare il tipo di errore senza cambiare il valore di successo. Se devi gestire entrambi i casi e restituire un singolo valore, usa fold. Per la registrazione senza interrompere la catena, usa onFailure (Kotlin) o un punto di ispezione prefisso.
Sì, ma con cautela. Result<T> di Kotlin non è raccomandato come tipo di ritorno diretto per funzioni suspend a causa di peculiarità del compilatore K2 e della riflessione. Usa la tua classe sealed personalizzata NetworkResult<T> (Success, Error, Loading) per rappresentare gli stati nelle coroutine. Per errori previsti nella logica di business, Either di Arrow è un'alternativa più potente.
fold è un metodo che accetta due callback: onSuccess (per il caso di successo) e onFailure (per il caso di errore), e restituisce un singolo valore di qualsiasi tipo. È l'equivalente di un'espressione switch ma come funzione di ordine superiore. fold è il punto di uscita principale dalle catene Result, dove converti Success/Failure in UiState, una stringa per l'utente o un altro Result.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche