Error Propagation é um mecanismo para propagar um erro para cima na pilha de chamadas do ponto de ocorrência até o manipulador. Quando uma função não consegue tratar um erro sozinha, ela o passa para a parte chamadora através de uma exceção (exception), declaração throws ou Return Type. A implementação correta da propagação é crítica para a estabilidade de aplicativos móveis: erros não tratados ou mal transmitidos levam a crashes. De acordo com Apple Swift Documentation (2026), a propagação automática via throws em Swift permite passar um erro para qualquer nível sem código boilerplate.
Pontos principais
Error Propagation é o processo de passar um objeto de erro da função onde ocorreu para cima na cadeia de chamadas até o manipulador adequado mais próximo. Imagine a pilha de chamadas: ViewController chama ViewModel, ViewModel chama Repository, Repository chama API. Se a API retornar um erro de rede, ele deve passar por Repository e ViewModel até ViewController, que exibirá uma mensagem ao usuário. Cada função intermediária decide: tratar o erro ou passá-lo adiante (propagar).
Existem duas abordagens para propagação: automática e manual. Com a abordagem automática (Swift throws, exceções checked do Java) o compilador obriga o desenvolvedor a tratar o erro ou declarar a propagação na assinatura. Com a abordagem manual (Result Type, Kotlin Try) o erro é passado como um valor — o desenvolvedor escreve explicitamente código para passar ou transformar o erro. De acordo com Kotlin Result Docs (2026), Result<T> em Kotlin não foi projetado para propagação direta entre limites de funções — ele precisa ser transformado ou tratado em cada nível, o que torna a propagação mais consciente, mas também mais verbosa.
A escolha da abordagem depende da arquitetura do aplicativo e da linguagem. Em Swift, a propagação automática via throws é dominante; em Kotlin, usa-se uma mistura de exceções (para erros inesperados) e contêineres tipo Result (para erros esperados). É importante entender: a propagação não é um objetivo, mas uma necessidade. Uma arquitetura ideal minimiza a profundidade da propagação, tratando erros no nível mais baixo possível onde haja contexto suficiente para tomar uma decisão.
Em Swift, a propagação via throws é automática: se a função A com throws chama a função B com throws, e A não trata o erro de B em do-catch, o erro é automaticamente passado para quem chama A. Isso elimina o código boilerplate típico das exceções checked do Java, onde throws deve ser declarado em cada método da cadeia. Swift usa o princípio “uma função throws na cadeia = toda a cadeia se torna throws, se não for tratada em níveis intermediários”.
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 — manipulador final
func onButtonTap() {
let vm = UserViewModel()
do {
let user = try vm.loadUser(id: 42)
updateUI(user)
} catch {
showError("Falha ao carregar o usuário")
}
}
Cadeia de propagação: networkService.request -> fetchUser -> loadUser -> onButtonTap. Cada função intermediária é marcada com throws e não contém do-catch — o erro é automaticamente passado para cima. ViewController onButtonTap é o manipulador final com do-catch. Se ViewModel decidisse transformar o erro (envolvê-lo em outro tipo), poderia usar do-catch e um novo throw. A propagação automática reduz o código: Repository não precisa saber como tratar o erro — essa é responsabilidade do ViewController, que tem acesso à UI para exibir uma mensagem ao usuário.
Em Kotlin, a propagação via exceções não requer uma declaração throws na assinatura (todas as exceções são unchecked). A exceção sobe automaticamente pela pilha até encontrar um try-catch. No entanto, a ausência de throws na assinatura torna a propagação implícita: o desenvolvedor não pode ver pela assinatura da função que ela pode lançar uma exceção. Isso é ao mesmo tempo uma vantagem (menos boilerplate) e uma desvantagem (mais fácil esquecer de tratar). Kotlin resolve esse problema por meio de convenções e padrões arquiteturais, não através da linguagem em si.
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")
}
}
}
}
Em Repository, propagação com transformação: ao ocorrer IOException (rede indisponível) a função tenta obter dados em cache do banco de dados. Se o cache estiver vazio, lança AppException — a propagação continua com um novo tipo de erro. ViewModel captura AppException e traduz para UiState.Error — o erro não vai além, a propagação termina no nível da camada de UI. Kotlin Coroutines adiciona particularidades: exceções em launch se propagam automaticamente através de CoroutineExceptionHandler, e em async — apenas quando await() é chamado. Isso é importante ao projetar propagação em corrotinas — SupervisorJob impede o cancelamento da corrotina pai diante de um erro em uma corrotina filha.
Uma alternativa às exceções é a propagação através de um tipo contêiner que passa sucesso ou erro como um valor. Nesta abordagem, a função não retorna um valor, mas um invólucro: Result<T, E> em Swift, Result<T> em Kotlin, Either<L, R> em Dart (do pacote fpdart ou dartz). O erro não desenrola a pilha — ele simplesmente está dentro do contêiner, e o próximo nível decide o que fazer com ele. Isso torna a propagação mais explícita e controlável.
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> é um contêiner simples com campos data e error. A classe selada AppError define os tipos de erro (Network, Auth). A função fetchUser retorna HttpResult, a propagação não requer desenrolar a pilha — quem chama simplesmente verifica isSuccess/isError. Esta abordagem é especialmente útil em Clean Architecture, onde cada camada (data, domain, presentation) pode transformar o erro: IOError -> DomainError -> UiError. A propagação via contêiner torna essas transformações explícitas e testáveis, ao contrário das exceções, onde a cadeia de transformações não é visível nas assinaturas das funções.
Uma das decisões-chave no design de tratamento de erros é escolher entre propagação (passar para cima) e tratamento (tratar aqui). A regra de decisão: trate o erro no nível onde houver contexto suficiente para uma ação significativa. Se você tem acesso à UI — mostre uma mensagem ao usuário. Se você tem acesso ao cache — tente se recuperar. Se não tem nem um nem outro — propague.
| Cenário | Ação | Justificativa |
|---|---|---|
| Erro de rede no Repository | Propagar | Repository não sabe se o usuário quer repetir a solicitação |
| Erro de análise no Repository | Tratar (retornar padrão) | Repository conhece o formato, pode retornar um valor de fallback |
| TimeOut no ViewModel | Tratar (UiState.Error) | ViewModel gerencia UiState, sabe como traduzir o erro |
| Erro de autorização no Interceptor | Tratar (atualizar token) | Interceptor tem acesso aos tokens e pode restaurar a sessão |
| Erro desconhecido no UseCase | Propagar | UseCase não tem contexto de UI — apenas lógica de negócio |
A regra de ouro: mínima propagação, máximo tratamento nos níveis inferiores. Se Repository pode se recuperar do cache — ele deve fazê-lo sem passar o erro para cima. Se ViewModel pode exibir um Snackbar — que o exiba, sem exigir código adicional de ViewController. Cada nível de propagação aumenta o acoplamento e complica os testes. De acordo com Google Android Architecture Guide (2026), recomenda-se minimizar a propagação através dos limites das camadas usando uma classe selada UiState para representar todos os estados possíveis (Loading, Success, Error) no nível ViewModel, e não passar exceções diretamente para a camada de UI.
A propagação incorreta é uma fonte de bugs difíceis de encontrar em aplicativos móveis. Vamos considerar cinco problemas principais que os desenvolvedores enfrentam e maneiras de resolvê-los.
O problema mais comum: durante a propagação, uma exceção é capturada, registrada e uma nova é lançada sem a exceção original. O desenvolvedor perde o StackTrace e não consegue entender onde exatamente o erro ocorreu. Em Swift, use encadeamento de erros: throw MyError(context: originalError). Em Kotlin: throw AppException(cause = originalException). Em Dart: throw AppException(message, originalException). Nunca crie uma nova exceção sem passar cause/underlyingError.
catch (e: Exception) { /* nada */ } é um antipadrão que faz o aplicativo continuar funcionando em um estado incorreto. Se você tem certeza de que o erro pode ser ignorado — adicione um comentário com a justificativa. Em Swift, use try? para ignorar opcionalmente (erro -> nil). Em Kotlin — Result<T>.onFailure { /* log */ }. Não engula exceções sem registrar.
Se um erro passa por 5+ níveis sem ser tratado, a arquitetura precisa de revisão. Cada nível de propagação é uma dependência da assinatura throws das funções subjacentes. Solução: use contêineres de Falha (sealed class Result { Success, Error }) nos limites das camadas para tornar a propagação explícita e limitada. Quanto mais curta a cadeia de propagação, mais fácil testar e depurar o código.
Em Kotlin Coroutines, uma exceção em launch por padrão cancela a corrotina pai e todos os siblings (filhos do mesmo escopo). Se uma de 10 tarefas paralelas falhar, as outras 9 serão canceladas, o que muitas vezes é indesejável. Use SupervisorJob ou supervisorScope para isolar erros: um erro em um filho não cancela os siblings. ViewModelScope usa SupervisorJob por padrão, o que evita esse problema no Android.
Em APIs baseadas em callbacks, o erro geralmente é passado como um parâmetro do callback. Se o callback não tratar o erro (ou tratá-lo incorretamente), a propagação se torna implícita e facilmente se perde. Solução: migre para async/await (Swift) ou corrotinas (Kotlin), onde a propagação funciona através de mecanismos padrão de try-catch. Se o callback for inevitável — use Either<Error, T> ou Result<T> para forçar o tratamento de ambos os casos.
Perguntas frequentes
Throw é uma ação única de lançar uma exceção. Error Propagation é todo o processo de passar um erro através de vários níveis da pilha, de throw até catch. A propagação inclui throw, a passagem automática ou manual através de funções intermediárias e o tratamento final. É um conceito mais amplo que descreve o ciclo de vida do erro.
Use objetos mock que lançam exceções em cenários determinados. Verifique se a função propaga ou trata corretamente o erro através de assertThrows (Kotlin/JUnit) ou XCTAssertThrowsError (Swift/XCTest). Para propagação baseada em Result, verifique isSuccess/isError e os valores em ambos os casos.
A propagação via Result é preferível para erros esperados (dados inválidos, regras de negócio) dentro de um limite arquitetural. Exceções são melhores para erros inesperados (perda de rede, erros de E/S) que devem ser tratados em um nível alto. Um resultado com erro não interrompe o fluxo de execução, uma exceção interrompe.
Em Kotlin Coroutines, uma exceção em launch se propaga automaticamente através de CoroutineScope com cancelamento de siblings. Use supervisorScope ou SupervisorJob para isolar: um erro em uma corrotina não cancela outras. Para async, o erro deve ser tratado explicitamente via try-catch ao chamar await(), caso contrário será engolido.
O manipulador final ideal é a camada de UI (ViewController, Fragment/Composable). Só ela tem acesso à interface do usuário e pode exibir uma mensagem, Snackbar ou diálogo. As camadas intermediárias (Repository, UseCase, ViewModel) propagam o erro, transformando-o se necessário em um tipo de domínio mais abstrato.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também