Error Propagation è un meccanismo che propaga un errore verso l'alto nello stack delle chiamate dal punto di origine fino al gestore. Quando una funzione non può gestire un errore da sola, lo passa al chiamante attraverso un'eccezione, una dichiarazione throws o un Return Type. L'implementazione corretta della propagazione è fondamentale per la stabilità delle applicazioni mobili: errori non gestiti o mal trasmessi portano a crash. Secondo Apple Swift Documentation (2026), la propagazione automatica tramite throws in Swift consente di passare un errore a qualsiasi livello senza codice boilerplate.
Punti chiave
Error Propagation (propagazione dell'errore) è il processo di passaggio di un oggetto errore dalla funzione in cui si è verificato verso l'alto nella catena di chiamate fino al gestore appropriato più vicino. Immagina lo stack delle chiamate: ViewController chiama ViewModel, ViewModel chiama Repository, Repository chiama API. Se l'API restituisce un errore di rete, deve passare attraverso Repository e ViewModel fino a ViewController, che mostrerà un messaggio all'utente. Ogni funzione intermedia decide: gestire l'errore o passarlo più in alto (propagare).
Esistono due approcci alla propagazione: automatico e manuale. Con l'approccio automatico (Swift throws, eccezioni verificate Java) il compilatore obbliga lo sviluppatore a gestire l'errore o a dichiarare la propagazione nella firma. Con l'approccio manuale (Result Type, Kotlin Try) l'errore viene passato come un valore — lo sviluppatore scrive esplicitamente codice per passare o trasformare l'errore. Secondo Kotlin Result Docs (2026), Result<T> in Kotlin non è progettato per la propagazione diretta attraverso i confini delle funzioni — deve essere trasformato o gestito a ogni livello, il che rende la propagazione più consapevole ma anche più verbosa.
La scelta dell'approccio dipende dall'architettura dell'applicazione e dal linguaggio. In Swift domina la propagazione automatica tramite throws, in Kotlin si usa un mix di eccezioni (per errori imprevisti) e contenitori simili a Result (per errori previsti). È importante capire: la propagazione non è un obiettivo, ma una necessità. Un'architettura ideale minimizza la profondità della propagazione, gestendo gli errori al livello più basso possibile in cui c'è abbastanza contesto per prendere una decisione.
In Swift, la propagazione tramite throws è automatica: se la funzione A con throws chiama la funzione B con throws, e A non gestisce l'errore di B in do-catch, l'errore viene automaticamente passato al chiamante di A. Questo elimina il codice boilerplate tipico delle eccezioni verificate Java, dove throws deve essere dichiarato in ogni metodo della catena. Swift usa il principio «una funzione throws nella catena = l'intera catena diventa throws, se non gestita a livelli intermedi».
struct UserRepository {
func fetchUser(id: Int) throws -> User {
let data = try networkService.request(path: "/users/\(id)")
return try parseUser(from: data)
}
}
class UserViewModel {
let repo = UserRepository()
func loadUser(id: Int) throws -> User {
return try repo.fetchUser(id: id)
}
}
// ViewController — gestore finale
func onButtonTap() {
let vm = UserViewModel()
do {
let user = try vm.loadUser(id: 42)
updateUI(user)
} catch {
showError("Caricamento utente fallito")
}
}
Catena di propagazione: networkService.request -> fetchUser -> loadUser -> onButtonTap. Ogni funzione intermedia è contrassegnata con throws e non contiene do-catch — l'errore viene automaticamente passato verso l'alto. ViewController onButtonTap è il gestore finale con do-catch. Se ViewModel decidesse di trasformare l'errore (avvolgerlo in un altro tipo), potrebbe usare do-catch e un nuovo throw. La propagazione automatica riduce il codice: Repository non ha bisogno di sapere come gestire l'errore — è responsabilità di ViewController, che ha accesso all'interfaccia utente per mostrare un messaggio all'utente.
In Kotlin, la propagazione tramite eccezioni non richiede una dichiarazione throws nella firma (tutte le eccezioni sono non verificate). L'eccezione risale automaticamente lo stack fino a quando non incontra un try-catch. Tuttavia, l'assenza di throws nella firma rende la propagazione implicita: lo sviluppatore non può vedere dalla firma della funzione che potrebbe lanciare un'eccezione. Questo è allo stesso tempo un vantaggio (meno boilerplate) e uno svantaggio (più facile dimenticare di gestirla). Kotlin risolve questo problema attraverso convenzioni e pattern architetturali, non attraverso il linguaggio stesso.
class UserRepository(
private val api: ApiService,
private val db: Database
) {
suspend fun getUser(id: String): User {
return try {
api.fetchUser(id)
} catch (e: IOException) {
db.getCachedUser(id) ?: throw AppException("User unavailable")
}
}
}
class UserViewModel(private val repo: UserRepository) {
private val _state = MutableStateFlow<UiState<User>>(UiState.Loading)
val state: StateFlow<UiState<User>> = _state
fun loadUser(id: String) {
viewModelScope.launch {
try {
val user = repo.getUser(id)
_state.value = UiState.Success(user)
} catch (e: AppException) {
_state.value = UiState.Error(e.message ?: "Unknown")
}
}
}
}
In Repository, propagazione con trasformazione: in caso di IOException (rete non disponibile) la funzione tenta di ottenere i dati memorizzati nella cache dal database. Se la cache è vuota, lancia AppException — la propagazione continua con un nuovo tipo di errore. ViewModel cattura AppException e lo traduce in UiState.Error — l'errore non va oltre, la propagazione termina a livello del layer UI. Kotlin Coroutines aggiunge particolarità: le eccezioni in launch si propagano automaticamente tramite CoroutineExceptionHandler, mentre in async — solo quando si chiama await(). Questo è importante quando si progetta la propagazione nelle coroutine — SupervisorJob impedisce la cancellazione della coroutine padre in caso di errore in una coroutine figlia.
Un'alternativa alle eccezioni è la propagazione attraverso un tipo contenitore che passa successo o errore come valore. In questo approccio, la funzione non restituisce un valore ma un wrapper: Result<T, E> in Swift, Result<T> in Kotlin, Either<L, R> in Dart (dal pacchetto fpdart o dartz). L'errore non svolge lo stack — si trova semplicemente nel contenitore, e il livello successivo decide cosa farne. Questo rende la propagazione più esplicita e controllabile.
data class HttpResult<out T>(
val data: T?,
val error: AppError?
) {
val isSuccess: Boolean get() = data != null
val isError: Boolean get() = error != null
}
sealed class AppError {
data class Network(val message: String) : AppError()
data class Auth(val message: String) : AppError()
}
fun fetchUser(id: String): HttpResult<User> {
return try {
val response = api.get("/users/$id")
HttpResult(data = parseUser(response), error = null)
} catch (e: IOException) {
HttpResult(data = null, error = AppError.Network("No internet"))
}
}
HttpResult<T> è un semplice contenitore con campi data ed error. La classe sealed AppError definisce i tipi di errore (Network, Auth). La funzione fetchUser restituisce HttpResult, la propagazione non richiede srotolamento dello stack — il chiamante verifica semplicemente isSuccess/isError. Questo approccio è particolarmente utile in Clean Architecture, dove ogni layer (data, domain, presentation) può trasformare l'errore: IOError -> DomainError -> UiError. La propagazione tramite contenitore rende queste trasformazioni esplicite e testabili, a differenza delle eccezioni, dove la catena di trasformazioni non è visibile nelle firme delle funzioni.
Una delle decisioni chiave nella progettazione della gestione degli errori è la scelta tra propagazione (passare verso l'alto) e gestione (gestire qui). La regola decisionale: gestisci l'errore al livello in cui c'è abbastanza contesto per un'azione significativa. Se hai accesso all'interfaccia utente — mostra un messaggio all'utente. Se hai accesso alla cache — prova a recuperare. Se non hai né l'uno né l'altro — propaga.
| Scenario | Azione | Motivazione |
|---|---|---|
| Errore di rete in Repository | Propaga | Repository non sa se l'utente vuole ripetere la richiesta |
| Errore di analisi in Repository | Gestisci (restituisci default) | Repository conosce il formato, può restituire un valore di fallback |
| Timeout in ViewModel | Gestisci (UiState.Error) | ViewModel gestisce UiState, sa come tradurre l'errore |
| Errore di autorizzazione in Interceptor | Gestisci (aggiorna token) | Interceptor ha accesso ai token e può ripristinare la sessione |
| Errore sconosciuto in UseCase | Propaga | UseCase non ha contesto UI — solo logica di business |
La regola d'oro: propagazione minima, gestione massima ai livelli inferiori. Se Repository può recuperare dalla cache — dovrebbe farlo senza passare l'errore verso l'alto. Se ViewModel può mostrare uno Snackbar — lo mostri, senza richiedere codice aggiuntivo da ViewController. Ogni livello di propagazione aumenta l'accoppiamento e complica i test. Secondo Google Android Architecture Guide (2026), si raccomanda di minimizzare la propagazione attraverso i confini dei layer usando una classe sealed UiState per rappresentare tutti gli stati possibili (Loading, Success, Error) a livello di ViewModel, e di non passare eccezioni direttamente al layer UI.
La propagazione errata è una fonte di bug difficili da trovare nelle applicazioni mobili. Consideriamo cinque problemi principali che gli sviluppatori devono affrontare e i modi per risolverli.
Il problema più comune: durante la propagazione, un'eccezione viene catturata, registrata e ne viene lanciata una nuova senza l'eccezione originale. Lo sviluppatore perde lo StackTrace e non può capire dove esattamente si è verificato l'errore. In Swift, usa concatenamento degli errori: throw MyError(context: originalError). In Kotlin: throw AppException(cause = originalException). In Dart: throw AppException(message, originalException). Non creare mai una nuova eccezione senza passare cause/underlyingError.
catch (e: Exception) { /* niente */ } è un antipattern che fa sì che l'applicazione continui a funzionare in uno stato errato. Se sei sicuro che l'errore possa essere ignorato — aggiungi un commento con la motivazione. In Swift, usa try? per ignorare opzionalmente (errore -> nil). In Kotlin — Result<T>.onFailure { /* log */ }. Non ingoiare eccezioni senza registrazione.
Se un errore attraversa 5+ livelli senza essere gestito, l'architettura necessita di revisione. Ogni livello di propagazione è una dipendenza dalla firma throws delle funzioni sottostanti. Soluzione: usa contenitori di Fallimento (sealed class Result { Success, Error }) ai confini dei layer per rendere la propagazione esplicita e limitata. Più breve è la catena di propagazione, più facile è testare e debuggare il codice.
In Kotlin Coroutines, un'eccezione in launch per impostazione predefinita annulla la coroutine padre e tutti i siblings (figli dello stesso scope). Se una delle 10 attività parallele fallisce, le altre 9 verranno annullate, cosa spesso indesiderabile. Usa SupervisorJob o supervisorScope per isolare gli errori: un errore in un figlio non annulla i siblings. ViewModelScope usa SupervisorJob per impostazione predefinita, il che evita questo problema in Android.
Nelle API basate su callback, l'errore viene spesso passato come parametro del callback. Se il callback non gestisce l'errore (o lo gestisce in modo errato), la propagazione diventa implicita e facilmente si perde. Soluzione: migra a async/await (Swift) o coroutine (Kotlin), dove la propagazione funziona attraverso meccanismi standard di try-catch. Se il callback è inevitabile — usa Either<Error, T> o Result<T> per forzare la gestione di entrambi i casi.
Domande frequenti
Throw è un'azione una tantum di lancio di un'eccezione. Error Propagation è l'intero processo di passaggio di un errore attraverso più livelli di stack, da throw a catch. La propagazione include throw, il passaggio automatico o manuale attraverso funzioni intermedie e la gestione finale. È un concetto più ampio che descrive il ciclo di vita dell'errore.
Usa oggetti mock che lanciano eccezioni in scenari determinati. Verifica che la funzione propaghi o gestisca correttamente l'errore tramite assertThrows (Kotlin/JUnit) o XCTAssertThrowsError (Swift/XCTest). Per la propagazione basata su Result, verifica isSuccess/isError e i valori in entrambi i casi.
La propagazione tramite Result è preferibile per errori previsti (dati non validi, regole di business) all'interno di un confine architetturale. Le eccezioni sono migliori per errori imprevisti (perdita di rete, errori I/O) che devono essere gestiti a un livello alto. Un risultato con errore non interrompe il flusso di esecuzione, un'eccezione sì.
In Kotlin Coroutines, un'eccezione in launch si propaga automaticamente attraverso CoroutineScope con cancellazione dei siblings. Usa supervisorScope o SupervisorJob per isolare: un errore in una coroutine non cancella le altre. Per async, l'errore deve essere gestito esplicitamente tramite try-catch quando si chiama await(), altrimenti verrà ingoiato.
Il gestore finale ideale è il layer UI (ViewController, Fragment/Composable). Solo esso ha accesso all'interfaccia utente e può mostrare un messaggio, Snackbar o finestra di dialogo. I layer intermedi (Repository, UseCase, ViewModel) propagano l'errore, trasformandolo se necessario in un tipo di dominio più astratto.
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