Error Propagation es un mecanismo para propagar un error hacia arriba en la pila de llamadas desde el punto de origen hasta el manejador. Cuando una función no puede manejar un error por sí misma, lo pasa a la parte llamante a través de una excepción, declaración throws o Return Type. La implementación correcta de la propagación es crítica para la estabilidad de las aplicaciones móviles: los errores no manejados o mal transmitidos provocan fallos. Según Apple Swift Documentation (2026), la propagación automática mediante throws en Swift permite pasar un error a cualquier nivel sin código repetitivo.
Puntos clave
Error Propagation es el proceso de pasar un objeto de error desde la función donde ocurrió hacia arriba en la cadena de llamadas hasta el manejador adecuado más cercano. Imagine la pila de llamadas: ViewController llama a ViewModel, ViewModel llama a Repository, Repository llama a API. Si la API devuelve un error de red, debe pasar a través de Repository y ViewModel hasta ViewController, que mostrará un mensaje al usuario. Cada función intermedia decide: manejar el error o pasarlo más arriba (propagar).
Existen dos enfoques para la propagación: automático y manual. Con el enfoque automático (Swift throws, excepciones checked de Java) el compilador obliga al desarrollador a manejar el error o declarar la propagación en la firma. Con el enfoque manual (Result Type, Kotlin Try) el error se pasa como un valor — el desarrollador escribe explícitamente código para pasar o transformar el error. Según Kotlin Result Docs (2026), Result<T> en Kotlin no está diseñado para propagación directa entre límites de funciones — debe transformarse o manejarse en cada nivel, lo que hace que la propagación sea más consciente pero también más verbosa.
La elección del enfoque depende de la arquitectura de la aplicación y del lenguaje. En Swift predomina la propagación automática mediante throws, en Kotlin se usa una mezcla de excepciones (para errores inesperados) y contenedores tipo Result (para errores esperados). Es importante entender: la propagación no es un objetivo, sino una necesidad. Una arquitectura ideal minimiza la profundidad de la propagación, manejando los errores en el nivel más bajo posible donde haya suficiente contexto para tomar una decisión.
En Swift, la propagación mediante throws es automática: si la función A con throws llama a la función B con throws, y A no maneja el error de B en do-catch, el error se pasa automáticamente a quien llama a A. Esto elimina el código repetitivo típico de las excepciones checked de Java, donde throws debe declararse en cada método de la cadena. Swift usa el principio “una función throws en la cadena = toda la cadena se vuelve throws, si no se maneja en niveles intermedios”.
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 — manejador final
func onButtonTap() {
let vm = UserViewModel()
do {
let user = try vm.loadUser(id: 42)
updateUI(user)
} catch {
showError("Error al cargar el usuario")
}
}
Cadena de propagación: networkService.request -> fetchUser -> loadUser -> onButtonTap. Cada función intermedia está marcada con throws y no contiene do-catch — el error se pasa automáticamente hacia arriba. ViewController onButtonTap es el manejador final con do-catch. Si ViewModel decidiera transformar el error (envolverlo en otro tipo), podría usar do-catch y un nuevo throw. La propagación automática reduce el código: Repository no necesita saber cómo manejar el error — esa es responsabilidad de ViewController, que tiene acceso a la UI para mostrar un mensaje al usuario.
En Kotlin, la propagación mediante excepciones no requiere una declaración throws en la firma (todas las excepciones son unchecked). La excepción sube automáticamente por la pila hasta que encuentra un try-catch. Sin embargo, la ausencia de throws en la firma hace que la propagación sea implícita: el desarrollador no puede ver desde la firma de la función que podría lanzar una excepción. Esto es tanto una ventaja (menos código repetitivo) como una desventaja (es más fácil olvidar manejarla) al mismo tiempo. Kotlin resuelve este problema mediante convenciones y patrones arquitectónicos, no a través del lenguaje en sí.
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")
}
}
}
}
En Repository, propagación con transformación: al ocurrir IOException (red no disponible) la función intenta obtener datos cacheados de la base de datos. Si la caché está vacía, lanza AppException — la propagación continúa con un nuevo tipo de error. ViewModel captura AppException y lo traduce a UiState.Error — el error no va más allá, la propagación termina a nivel de la capa de UI. Kotlin Coroutines añade particularidades: las excepciones en launch se propagan automáticamente a través de CoroutineExceptionHandler, y en async — solo cuando se llama a await(). Esto es importante al diseñar propagación en corrutinas — SupervisorJob evita la cancelación de la corrutina padre ante un error en una corrutina hija.
Una alternativa a las excepciones es la propagación a través de un tipo contenedor que pasa el éxito o el error como un valor. En este enfoque, la función no devuelve un valor sino un envoltorio: Result<T, E> en Swift, Result<T> en Kotlin, Either<L, R> en Dart (del paquete fpdart o dartz). El error no desenrolla la pila — simplemente está dentro del contenedor, y el siguiente nivel decide qué hacer con él. Esto hace que la propagación sea más explícita y controlable.
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> es un contenedor simple con campos data y error. La clase sellada AppError define los tipos de error (Network, Auth). La función fetchUser devuelve HttpResult, la propagación no requiere desenrollar la pila — la parte llamante simplemente verifica isSuccess/isError. Este enfoque es especialmente útil en Clean Architecture, donde cada capa (data, domain, presentation) puede transformar el error: IOError -> DomainError -> UiError. La propagación mediante contenedor hace que estas transformaciones sean explícitas y comprobables, a diferencia de las excepciones, donde la cadena de transformaciones no es visible en las firmas de las funciones.
Una de las decisiones clave en el diseño del manejo de errores es elegir entre propagación (pasar hacia arriba) y manejo (manejar aquí). La regla de decisión: maneja el error en el nivel donde haya suficiente contexto para una acción significativa. Si tienes acceso a la UI — muestra un mensaje al usuario. Si tienes acceso a la caché — intenta recuperarte. Si no tienes ni lo uno ni lo otro — propaga.
| Escenario | Acción | Justificación |
|---|---|---|
| Error de red en Repository | Propagar | Repository no sabe si el usuario quiere reintentar la solicitud |
| Error de análisis en Repository | Manejar (devolver valor por defecto) | Repository conoce el formato, puede devolver un valor de respaldo |
| TimeOut en ViewModel | Manejar (UiState.Error) | ViewModel gestiona UiState, sabe cómo traducir el error |
| Error de autorización en Interceptor | Manejar (refrescar token) | Interceptor tiene acceso a los tokens y puede restaurar la sesión |
| Error desconocido en UseCase | Propagar | UseCase no tiene contexto de UI — solo lógica de negocio |
La regla de oro: mínima propagación, máximo manejo en los niveles inferiores. Si Repository puede recuperarse de la caché — debe hacerlo sin pasar el error hacia arriba. Si ViewModel puede mostrar un Snackbar — que lo muestre, sin requerir código adicional de ViewController. Cada nivel de propagación aumenta el acoplamiento y complica las pruebas. Según Google Android Architecture Guide (2026), se recomienda minimizar la propagación a través de los límites de las capas usando una clase sellada UiState para representar todos los estados posibles (Loading, Success, Error) a nivel de ViewModel, y no pasar excepciones directamente a la capa de UI.
La propagación incorrecta es una fuente de errores difíciles de encontrar en aplicaciones móviles. Consideremos cinco problemas principales que enfrentan los desarrolladores y formas de resolverlos.
El problema más común: durante la propagación, se captura una excepción, se registra y se lanza una nueva sin la excepción original. El desarrollador pierde el StackTrace y no puede entender dónde ocurrió exactamente el error. En Swift, use encadenamiento de errores: throw MyError(context: originalError). En Kotlin: throw AppException(cause = originalException). En Dart: throw AppException(message, originalException). Nunca cree una nueva excepción sin pasar cause/underlyingError.
catch (e: Exception) { /* nada */ } es un antipatrón que hace que la aplicación continúe funcionando en un estado incorrecto. Si está seguro de que el error se puede ignorar — añada un comentario con la justificación. En Swift, use try? para ignorarlo opcionalmente (error -> nil). En Kotlin — Result<T>.onFailure { /* log */ }. No trague excepciones sin registrar.
Si un error pasa por 5 o más niveles sin ser manejado, la arquitectura necesita revisión. Cada nivel de propagación es una dependencia de la firma throws de las funciones subyacentes. Solución: use contenedores de Fallo (sealed class Result { Success, Error }) en los límites de las capas para que la propagación sea explícita y limitada. Cuanto más corta sea la cadena de propagación, más fácil será probar y depurar el código.
En Kotlin Coroutines, una excepción en launch por defecto cancela la corrutina padre y todos los siblings (hijos del mismo ámbito). Si una de 10 tareas paralelas falla, las otras 9 se cancelarán, lo que a menudo no es deseable. Use SupervisorJob o supervisorScope para aislar errores: un error en un hijo no cancela a los siblings. ViewModelScope usa SupervisorJob por defecto, lo que evita este problema en Android.
En las API basadas en callbacks, el error a menudo se pasa como un parámetro del callback. Si el callback no maneja el error (o lo maneja incorrectamente), la propagación se vuelve implícita y se pierde fácilmente. Solución: migre a async/await (Swift) o corrutinas (Kotlin), donde la propagación funciona mediante mecanismos estándar de try-catch. Si el callback es inevitable — use Either<Error, T> o Result<T> para forzar el manejo de ambos casos.
Preguntas frecuentes
Throw es una acción única de lanzar una excepción. Error Propagation es todo el proceso de pasar un error a través de varios niveles de la pila, desde throw hasta catch. La propagación incluye throw, el paso automático o manual a través de funciones intermedias y el manejo final. Es un concepto más amplio que describe el ciclo de vida del error.
Use objetos mock que lanzan excepciones en escenarios determinados. Verifique que la función propaga o maneja correctamente el error mediante assertThrows (Kotlin/JUnit) o XCTAssertThrowsError (Swift/XCTest). Para la propagación basada en Result, verifique isSuccess/isError y los valores en ambos casos.
La propagación mediante Result es preferible para errores esperados (datos inválidos, reglas de negocio) dentro de un límite arquitectónico. Las excepciones son mejores para errores inesperados (pérdida de red, errores de E/S) que deben manejarse a un alto nivel. Un resultado con error no interrumpe el flujo de ejecución, una excepción sí.
En Kotlin Coroutines, una excepción en launch se propaga automáticamente a través de CoroutineScope con cancelación de siblings. Use supervisorScope o SupervisorJob para aislar: un error en una corrutina no cancela otras. Para async, el error debe manejarse explícitamente mediante try-catch al llamar a await(), de lo contrario se tragará.
El manejador final ideal es la capa de UI (ViewController, Fragment/Composable). Solo ella tiene acceso a la interfaz de usuario y puede mostrar un mensaje, Snackbar o diálogo. Las capas intermedias (Repository, UseCase, ViewModel) propagan el error, transformándolo si es necesario a un tipo de dominio más abstracto.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también