Handling errors properly when using Retrofit in Kotlin is crucial for delivering a reliable and user-friendly Android application. Whether you’re dealing with network failures, parsing errors, or API-specific error responses, following a structured error-handling pattern helps ensure your app remains robust and user-friendly.

Retrofit is one of the most widely used HTTP clients for Android and provides a simple way to make network requests. However, beginners often overlook comprehensive error handling, which leads to unexpected crashes or poor user experience.
Below are best practices for error handling in Kotlin using Retrofit, including complete sample code divided by class and interface.
1. Use a Sealed Class for API Result
// ApiResult.kt sealed class ApiResult<out T> { data class Success<out T>(val data: T): ApiResult<T>() data class Error(val exception: Throwable): ApiResult<Nothing>() }
2. Retrofit Service Interface
// ApiService.kt interface ApiService { @GET("users/{id}") suspend fun getUser(@Path("id") id: Int): Response<User> }
3. API Call Wrapper in RepositoryAPI Call Wrapper in Repository
// UserRepository.kt class UserRepository(private val apiService: ApiService) { suspend fun getUserById(id: Int): ApiResult<User> { return try { val response = apiService.getUser(id) if (response.isSuccessful) { response.body()?.let { ApiResult.Success(it) } ?: ApiResult.Error(Exception("Empty body")) } else { ApiResult.Error(Exception("HTTP ${response.code()} ${response.message()}")) } } catch (e: IOException) { ApiResult.Error(Exception("Network Error: ${e.localizedMessage}")) } catch (e: Exception) { ApiResult.Error(e) } } }
4. ViewModel Usage
// UserViewModel.kt class UserViewModel(private val repository: UserRepository): ViewModel() { private val _userLiveData = MutableLiveData<ApiResult<User>>() val userLiveData: LiveData<ApiResult<User>> = _userLiveData fun fetchUser(id: Int) { viewModelScope.launch { val result = repository.getUserById(id) _userLiveData.postValue(result) } } }
5. UI Layer Observation
// In Activity or Fragment viewModel.userLiveData.observe(viewLifecycleOwner) { result -> when(result) { is ApiResult.Success -> { // Show user data } is ApiResult.Error -> { // Show error message Toast.makeText(context, result.exception.message, Toast.LENGTH_SHORT).show() } } }
Best Practice Summary
- Always catch
IOException
and generalException
. - Use
Response.isSuccessful
to check HTTP success. - Return a consistent result with a sealed class.
- Show meaningful errors to users with fallback messages.
For a more advanced approach, consider implementing Retrofit’s custom call adapter.
By applying these patterns, you improve app resilience, make testing easier, and provide a smoother user experience.