Error Propagation: qué es, mecanismo de propagación de errores y cómo funciona en el desarrollo móvil

Autor: IT Sectr Publicado: 2026-05-26 Tiempo de lectura: 9 min

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 pasar un error desde donde ocurrió hacia arriba en la pila hasta un manejador, omitiendo funciones intermedias
  • La propagación automática mediante throws en Swift pasa el error sin código explícito en cada nivel de la pila
  • La propagación manual en Kotlin y Dart requiere try-catch explícito o paso en un contenedor Result en cada nivel
  • Las excepciones checked en Java fuerzan la propagación mediante throws en la firma, las unchecked se pueden ignorar
  • Result Type es una alternativa a las excepciones donde el error se pasa como un valor sin desenrollar la pila

¿Qué es Error Propagation?

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.

Propagación mediante Throws en Swift

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

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

Propagación mediante excepciones en Kotlin

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

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

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.

Propagación mediante Result Type

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.

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

Propagación vs Manejo: cuándo pasar, cuándo manejar

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.

EscenarioAcciónJustificación
Error de red en RepositoryPropagarRepository no sabe si el usuario quiere reintentar la solicitud
Error de análisis en RepositoryManejar (devolver valor por defecto)Repository conoce el formato, puede devolver un valor de respaldo
TimeOut en ViewModelManejar (UiState.Error)ViewModel gestiona UiState, sabe cómo traducir el error
Error de autorización en InterceptorManejar (refrescar token)Interceptor tiene acceso a los tokens y puede restaurar la sesión
Error desconocido en UseCasePropagarUseCase 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.

Problemas y antipatrones de Error Propagation

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.

Pérdida de contexto del error

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.

Ignorar el error (catch vacío)

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.

Profundidad excesiva de propagación

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.

Propagación en corrutinas sin SupervisorJob

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.

Propagación mediante callback sin manejo

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

¿En qué se diferencia Error Propagation de throw?

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.

¿Cómo probar Error Propagation?

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.

¿Cuándo es mejor la propagación mediante Result que las excepciones?

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

¿Cómo propagar un error a través de corrutinas Kotlin?

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

¿Qué nivel debería ser el manejador final de errores?

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

  • Error Propagation es pasar un error hacia arriba en la pila desde donde ocurrió hasta un manejador mediante excepciones o contenedores Result
  • La propagación automática en Swift mediante throws no requiere código en niveles intermedios — el error sube solo
  • La propagación manual en Kotlin mediante try-catch explícito y throw en cada nivel hace que el manejo sea consciente pero verboso
  • Result Type pasa el error como un valor sin desenrollar la pila, conveniente para errores esperados en Clean Architecture
  • Regla de manejo: manejar en el nivel con contexto (UI), propagar a través de niveles sin contexto (domain, data)
  • Antipatrones: catch vacío, pérdida de cause en la transformación, profundidad excesiva de propagación, ignorar SupervisorJob
  • Diseñe errores tipificados (sealed class / enum) para cada capa y transfórmelos al cruzar los límites de las capas

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.

Discutir el proyecto

Lea también