TecMeli implements a comprehensive error handling strategy that transforms low-level technical exceptions into user-friendly messages. The system separates network errors, timeout issues, server responses, and unexpected failures into distinct categories.
All application errors extend from the AppError sealed class:
sealed class AppError : Exception() { /** Error de conectividad de red (sin internet o problemas de conexión). */ class Network : AppError() /** Tiempo de espera agotado al conectar con el servidor. */ class Timeout : AppError() /** * Error retornado por el servidor (4xx, 5xx). * @property code Código HTTP de la respuesta. * @property msg Mensaje de error retornado por el servidor o descripción técnica. */ data class Server(val code: Int, val msg: String) : AppError() /** * Error no clasificado o excepción inesperada durante la ejecución. * @property throwable La causa original del error para propósitos de depuración. */ data class Unknown(val throwable: Throwable) : AppError()}
The ErrorMapper transforms raw exceptions into domain errors:
interface ErrorMapper { /** * Transforms a low-level exception into an AppError object * * @param exception The exception thrown during an operation * @return An application-specific error representation */ fun mapException(exception: Exception): AppError}
class DefaultErrorMapper : ErrorMapper { override fun mapException(exception: Exception): AppError = when (exception) { is SocketTimeoutException -> AppError.Timeout() is IOException -> AppError.Network() else -> AppError.Unknown(exception) }}
See core/network/mapper/DefaultErrorMapper.kt:14
The mapper follows the Open/Closed Principle: you can extend error types by creating a custom ErrorMapper implementation without modifying existing code.
For better user experience, you can map AppError types to specific messages:
fun AppError.toUserMessage(): String = when (this) { is AppError.Network -> "No hay conexión a internet. Verifica tu red y vuelve a intentar." is AppError.Timeout -> "El servidor tardó demasiado en responder. Inténtalo de nuevo." is AppError.Server -> when (code) { 401 -> "Tu sesión ha expirado. Por favor, inicia sesión nuevamente." 404 -> "El recurso solicitado no fue encontrado." 500 -> "Error del servidor. Inténtalo más tarde." else -> "Error del servidor: $msg" } is AppError.Unknown -> "Ocurrió un error inesperado: ${throwable.localizedMessage}"}// Usage in ViewModel.onFailure { error -> _uiState.value = UiState.Error( message = (error as? AppError)?.toUserMessage() ?: "Ocurrió un error inesperado", exception = error )}
Network Error
Show a message prompting the user to check their connection, with a retry button.
Timeout Error
Inform the user the server is slow and offer a retry option.
401 Unauthorized
Navigate the user to the login screen or trigger token refresh.
404 Not Found
Show a friendly “not found” message with suggestions.
500+ Server Errors
Display a generic server error message and log the details for debugging.
@Testfun `searchProducts handles network error`() = runTest { val errorMapper = DefaultErrorMapper() val networkException = IOException("No internet") val appError = errorMapper.mapException(networkException) // Verify mapping assertTrue(appError is AppError.Network)}@Testfun `ViewModel emits Error state on failure`() = runTest { val fakeUseCase = FakeGetProductsUseCase( result = Result.failure(AppError.Network()) ) val viewModel = HomeViewModel(fakeUseCase) viewModel.searchProducts("phone") val state = viewModel.uiState.value assertTrue(state is UiState.Error) assertEquals("No hay conexión a internet", (state as UiState.Error).message)}