A suspend Retrofit 43,944 call returns the body or throws: HttpException for a 4xx or 5xx status, IOException when no answer arrives, SerializationException when the body doesn't match the DTO. Rather than scatter try/catch through every caller, BookNest converts them once into a sealed type, like LoadState (UI State with Sealed Classes):
sealed interface ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>
data class HttpError(val code: Int) : ApiResult<Nothing> // the server said no
data class NetworkError(val cause: IOException) : ApiResult<Nothing> // no answer at all
}
suspend fun <T> apiCall(block: suspend () -> T): ApiResult<T> = try {
ApiResult.Success(block())
} catch (e: HttpException) { // a 4xx or 5xx status
ApiResult.HttpError(e.code())
} catch (e: IOException) { // offline, refused, timed out
ApiResult.NetworkError(e)
} // CancellationException passes throughThe compiler makes every when over the result handle all three cases. Never catch Exception here: cancellation is a CancellationException, and swallowing it keeps a cancelled screen's work running (runCatching has the same flaw). Serialization errors crash on purpose: they are DTO bugs, not user conditions. With the test server answering 503, and then stopped, the repository logged:
W/BookNestNet: refresh failed: HTTP 503 ... W/BookNestNet: offline: java.net.ConnectException: Failed to connect to /10.0.2.2:8616
Put retries where the user or system asks for them (a Retry button, WorkManager 234 's backoff in Constraints and Chaining), not in a hidden loop inside every call.