Error Handling
Architecture
Data Layer Domain Layer Presentation Layer
ExceptionMapper --> AppError (sealed) --> ErrorHandler
(Exception -> domain) (in DataState) (AppError -> string res)
AppError (domain/core)
sealed class AppError {
abstract val loggableMessage: String? // For logging ONLY, never show to users
// Network
data class NoInternetConnection(...) : AppError()
data class NetworkTimeout(...) : AppError()
data class ServerError(val statusCode: Int, ...) : AppError()
// Data
data class DataParsingError(val exception: Exception, ...) : AppError()
// Auth
data class Unauthorized(...) : AppError()
// Generic
data class Unknown(val exception: Throwable? = null, ...) : AppError()
}
ExceptionMapper (data/core)
Maps infrastructure exceptions to AppError. Used internally by BaseDataSource strategies — do NOT call from presentation layer.
SocketTimeoutException -> AppError.NetworkTimeout
IOException -> AppError.NoInternetConnection
HttpException(401) -> AppError.Unauthorized
HttpException(400-499) -> AppError.ServerError(statusCode)
HttpException(500-599) -> AppError.ServerError(statusCode)
JsonEncodingException -> AppError.DataParsingError
JsonDataException -> AppError.DataParsingError
Other -> AppError.Unknown
ErrorHandler (feature/core)
fun getErrorMessageResId(error: AppError): Int = when (error) {
is AppError.NoInternetConnection -> R.string.no_connect_internet
is AppError.NetworkTimeout -> R.string.network_timeout
is AppError.Unauthorized -> R.string.unauthorized
is AppError.ServerError -> R.string.server_error
is AppError.DataParsingError -> R.string.data_error
// ...
}
Error Flow Patterns
Pattern 1: DataState Flow (most common)
// Data layer handles automatically via strategy classes
// ViewModel:
viewModelScope.launch {
collectDataStateWithInternet(
callFlow = useCase(params),
data -> setState { copy(detail = data) } },
error -> showErrorToast(error) },
)
}
Pattern 2: PagingData Flow
val result = callPagingDataWithInternet(
callFlow = { useCase() },
error -> showErrorToast(error) },
).cachedIn(viewModelScope)
Pattern 3: Suspend operations
viewModelScope.launch {
callSuspendWithInternet(
operation = { useCase(params) },
setState { copy(saved = true) } },
error -> showErrorToast(error) },
)
}
When to Use Which Function
| Scenario | Function | Internet Check |
|---|---|---|
| PagingData from network | callPagingDataWithInternet() |
Yes |
| PagingData from local | callPagingDataWithoutInternet() |
No |
| DataState flow from network | collectDataStateWithInternet() |
Yes |
| DataState flow from local | collectDataState() |
No |
| One-shot network operation | callSuspendWithInternet() |
Yes |
| One-shot local operation | callSuspendWithoutInternet() |
No |
| Regular flow collection | collectFlowWithInternet() |
Yes |
Adding a New Error Type
- Add to
AppErrorindomain/core:
data class NewErrorType(
override val loggableMessage: String? = "Description"
) : AppError()
- Map in
ExceptionMapper(if from infrastructure exception):
is MyCustomException -> AppError.NewErrorType(...)
- Map in
ErrorHandler:
is AppError.NewErrorType -> R.string.new_error_message
- Add string resource in
feature/core/src/main/res/values/strings.xml
Toast Display
// In ViewModel - automatically maps AppError -> localized string -> toast
showErrorToast(appError) // Red toast
showSuccessToast(R.string.saved) // Green toast
showWarningToast(R.string.warning) // Yellow toast
showInfoToast(R.string.info) // Blue toast
Toast displayed via ToastHost provided by TemplateTheme.