Error Handling Patterns in Compose: From Try-Catch to Graceful UX
Error HandlingUXBest PracticesArchitecture
Errors Are Inevitable
Network requests fail. Users enter invalid data. APIs return unexpected responses. A production-quality app handles all these gracefully, turning potential crashes into helpful user experiences.
Compose makes error handling elegant when you model errors as state. Instead of try-catch scattered everywhere, errors become first-class citizens in your UI state.
The Sealed Result Pattern
Model operation outcomes with a sealed class that captures success, loading, and error states:
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Throwable) : Result<Nothing>()
object Loading : Result<Nothing>()
}
// In ViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
private val _userState = MutableStateFlow<Result<User>>(Result.Loading)
val userState: StateFlow<Result<User>> = _userState.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
_userState.value = Result.Loading
_userState.value = try {
Result.Success(userRepository.getUser(userId))
} catch (e: Exception) {
Result.Error(e)
}
}
}
}Rendering Error States
Create reusable components for different result states:
@Composable
fun <T> ResultHandler(
result: Result<T>,
onRetry: () -> Unit,
content: @Composable (T) -> Unit
) {
when (result) {
is Result.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is Result.Error -> {
ErrorScreen(
message = result.exception.toUserMessage(),
onRetry = onRetry
)
}
is Result.Success -> {
content(result.data)
}
}
}
// Extension function for user-friendly messages
fun Throwable.toUserMessage(): String = when (this) {
is java.net.UnknownHostException -> "No internet connection"
is java.net.SocketTimeoutException -> "Request timed out"
is HttpException -> when (code()) {
401 -> "Please log in again"
403 -> "Access denied"
404 -> "Content not found"
in 500..599 -> "Server error. Please try later."
else -> "Something went wrong"
}
else -> "An unexpected error occurred"
}Form Validation Errors
For form inputs, model validation errors alongside the data:
data class FormField(
val value: String = "",
val error: String? = null
)
data class LoginFormState(
val email: FormField = FormField(),
val password: FormField = FormField(),
val isSubmitting: Boolean = false
)
class LoginViewModel : ViewModel() {
private val _formState = MutableStateFlow(LoginFormState())
val formState: StateFlow<LoginFormState> = _formState.asStateFlow()
fun updateEmail(email: String) {
_formState.update {
it.copy(email = FormField(value = email, error = validateEmail(email)))
}
}
private fun validateEmail(email: String): String? {
return when {
email.isBlank() -> "Email is required"
!email.contains("@") -> "Invalid email format"
else -> null
}
}
}
// In Compose
@Composable
fun EmailField(field: FormField, onValueChange: (String) -> Unit) {
Column {
OutlinedTextField(
value = field.value,
onValueChange = onValueChange,
isError = field.error != null,
label = { Text("Email") }
)
if (field.error != null) {
Text(
text = field.error,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
)
}
}
}Snackbars for Transient Errors
Some errors are best shown briefly and dismissed automatically:
@Composable
fun AppScaffold(viewModel: AppViewModel) {
val snackbarHostState = remember { SnackbarHostState() }
// Collect error events
LaunchedEffect(Unit) {
viewModel.errorEvents.collect { error ->
val result = snackbarHostState.showSnackbar(
message = error.message,
actionLabel = if (error.isRetryable) "Retry" else null,
duration = SnackbarDuration.Short
)
if (result == SnackbarResult.ActionPerformed) {
error.onRetry?.invoke()
}
}
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
AppContent(Modifier.padding(padding))
}
}Note: Match error severity to UI treatment: blocking errors get full-screen treatment, recoverable errors get snackbars, field errors appear inline.