Error Propagation: cos'è, meccanismo di propagazione degli errori e come funziona nello sviluppo mobile

Autore: IT Sectr Pubblicato: 2026-05-26 Tempo di lettura: 9 min

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 — passaggio di un errore dal luogo di origine verso l'alto dello stack a un gestore, saltando le funzioni intermedie
  • Propagazione automatica tramite throws in Swift passa l'errore senza codice esplicito a ogni livello dello stack
  • Propagazione manuale in Kotlin e Dart richiede try-catch esplicito o passaggio in un contenitore Result a ogni livello
  • Eccezioni verificate in Java impongono la propagazione tramite throws nella firma, quelle non verificate possono essere ignorate
  • Result Type — un'alternativa alle eccezioni in cui l'errore viene passato come valore senza srotolamento dello stack

Cos'è Error Propagation?

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.

Propagazione tramite Throws in Swift

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».

swift
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.

Propagazione tramite eccezioni in Kotlin

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.

kotlin
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.

Propagazione tramite Result Type

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.

kotlin
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.

Propagazione vs Gestione: quando passare, quando gestire

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.

ScenarioAzioneMotivazione
Errore di rete in RepositoryPropagaRepository non sa se l'utente vuole ripetere la richiesta
Errore di analisi in RepositoryGestisci (restituisci default)Repository conosce il formato, può restituire un valore di fallback
Timeout in ViewModelGestisci (UiState.Error)ViewModel gestisce UiState, sa come tradurre l'errore
Errore di autorizzazione in InterceptorGestisci (aggiorna token)Interceptor ha accesso ai token e può ripristinare la sessione
Errore sconosciuto in UseCasePropagaUseCase 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.

Problemi e antipattern di Error Propagation

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.

Perdita del contesto dell'errore

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.

Ignorare l'errore (catch vuoto)

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.

Profondità eccessiva di propagazione

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.

Propagazione nelle coroutine senza SupervisorJob

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.

Propagazione tramite callback senza gestione

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

In che modo Error Propagation è diverso da throw?

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.

Come testare Error Propagation?

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.

Quando la propagazione tramite Result è migliore delle eccezioni?

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ì.

Come propagare un errore attraverso le coroutine Kotlin?

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.

Quale livello dovrebbe essere il gestore finale dell'errore?

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

  • Error Propagation — passaggio di un errore verso l'alto dello stack dal luogo di origine a un gestore tramite eccezioni o contenitori Result
  • Propagazione automatica in Swift tramite throws non richiede codice a livelli intermedi — l'errore sale da solo
  • Propagazione manuale in Kotlin tramite try-catch esplicito e throw a ogni livello rende la gestione consapevole ma verbosa
  • Result Type passa l'errore come valore senza srotolamento dello stack, comodo per errori previsti in Clean Architecture
  • Regola di gestione: gestisci al livello con contesto (UI), propaga attraverso livelli senza contesto (domain, data)
  • Antipattern: catch vuoto, perdita di cause nella trasformazione, profondità eccessiva di propagazione, ignorare SupervisorJob
  • Progetta errori tipizzati (sealed class / enum) per ogni layer e trasformali quando attraversano i confini dei layer

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.

Discuti il progetto

Leggi anche