Error Propagation: o que é, mecanismo de propagação de erros e como funciona no desenvolvimento mobile

Autor: IT Sectr Publicado: 2026-05-26 Tempo de leitura: 9 min

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 é passar um erro de onde ocorreu para cima na pilha até um manipulador, ignorando funções intermediárias
  • Propagação automática via throws em Swift passa o erro sem código explícito em cada nível da pilha
  • Propagação manual em Kotlin e Dart requer try-catch explícito ou passagem em um contêiner Result em cada nível
  • Exceções checked em Java forçam a propagação via throws na assinatura, unchecked podem ser ignoradas
  • Result Type é uma alternativa às exceções onde o erro é passado como um valor sem desenrolar a pilha

O que é Error Propagation?

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.

Propagação via Throws em Swift

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

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

Propagação via exceções em Kotlin

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.

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")
            }
        }
    }
}

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.

Propagação via Result Type

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.

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

Propagação vs Tratamento: quando passar, quando tratar

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árioAçãoJustificativa
Erro de rede no RepositoryPropagarRepository não sabe se o usuário quer repetir a solicitação
Erro de análise no RepositoryTratar (retornar padrão)Repository conhece o formato, pode retornar um valor de fallback
TimeOut no ViewModelTratar (UiState.Error)ViewModel gerencia UiState, sabe como traduzir o erro
Erro de autorização no InterceptorTratar (atualizar token)Interceptor tem acesso aos tokens e pode restaurar a sessão
Erro desconhecido no UseCasePropagarUseCase 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.

Problemas e antipadrões de Error Propagation

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.

Perda de contexto do erro

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.

Ignorar o erro (catch vazio)

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.

Profundidade excessiva de propagação

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.

Propagação em corrotinas sem SupervisorJob

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.

Propagação via callback sem tratamento

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

Como Error Propagation difere de throw?

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.

Como testar Error Propagation?

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.

Quando a propagação via Result é melhor que exceções?

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.

Como propagar um erro através de corrotinas Kotlin?

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.

Qual nível deve ser o manipulador final de erro?

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

  • Error Propagation é passar um erro para cima na pilha de onde ocorreu até um manipulador via exceções ou contêineres Result
  • Propagação automática em Swift via throws não requer código em níveis intermediários — o erro sobe sozinho
  • Propagação manual em Kotlin via try-catch explícito e throw em cada nível torna o tratamento consciente, mas verboso
  • Result Type passa o erro como um valor sem desenrolar a pilha, conveniente para erros esperados em Clean Architecture
  • Regra de tratamento: trate no nível com contexto (UI), propague através de níveis sem contexto (domain, data)
  • Antipadrões: catch vazio, perda de cause na transformação, profundidade excessiva de propagação, ignorar SupervisorJob
  • Projete erros tipificados (sealed class / enum) para cada camada e transforme-os ao cruzar os limites das camadas

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.

Discutir o projeto

Leia também