Kahibaro
Discord Login Register

19.3 Error Handling

Understanding Error Handling with Retrofit

When you connect your app to a server, things will fail sometimes. The network can be offline, the server can return an error, or your JSON might not match the expected format. Error handling with Retrofit is about detecting those problems, understanding what went wrong, and reacting in a way that keeps your app stable and user friendly.

In this chapter, you will focus on how to identify different kinds of errors when using Retrofit and how to react to them in code. You will not design the whole networking layer or architecture here, but you will learn how to interpret and handle the various failures that Retrofit can produce.

Types of Errors in Retrofit Calls

A Retrofit call can fail in different ways. To handle errors correctly you need to distinguish between network level failures, HTTP response errors, and unexpected problems such as parsing issues or bugs in your code.

When you use a Retrofit Call<T> and execute it, typically through enqueue, Retrofit gives you two main paths. If the call reached the server and a response was received, onResponse is called. If something prevented a response, onFailure is called.

Inside onResponse, you still need to check whether the HTTP status code represents success. Retrofit considers any reachable response a success at the transport level, even if the server replied with a 404 or 500. Only onFailure is used for lower level failures, such as timeouts or no internet connection.

Handling Errors with Call.enqueue

The most common way to use Retrofit in basic apps is to call enqueue on a Call<T> object. This registers callbacks for success and failure.

A typical pattern looks like this:

apiService.getUserProfile().enqueue(object : Callback<UserProfile> {
    override fun onResponse(
        call: Call<UserProfile>,
        response: Response<UserProfile>
    ) {
        if (response.isSuccessful) {
            val body = response.body()
            if (body != null) {
                // Use the successful result
            } else {
                // Handle empty body as an application error
            }
        } else {
            // Handle HTTP error response
            val code = response.code()
            val errorBody = response.errorBody()?.string()
            // Show error message or map code to user friendly info
        }
    }
    override fun onFailure(call: Call<UserProfile>, t: Throwable) {
        // Network error, timeout, DNS failure, or unexpected exception
        // Use t to identify what went wrong
    }
})

In onResponse, you always check response.isSuccessful. This property is true for HTTP codes in the 200 range. If it is false, you have an HTTP error, such as 400, 401, 404, or 500. You should not treat this as a crash but as a usable error condition.

In onFailure, you get a Throwable. You can use instanceof style checks in Kotlin with the is operator to determine what kind of failure occurred, and then respond accordingly.

Differentiating HTTP Errors and Network Failures

It is important to understand that not all failures are the same. Network failures usually mean the device could not reach the server at all. HTTP errors mean the server responded but with a non successful code.

Network failures are delivered to onFailure. These can include no internet connection, timeouts, SSL handshake errors, and similar issues. They often come from OkHttp, the networking client that Retrofit uses.

You can detect these with code like this:

override fun onFailure(call: Call<UserProfile>, t: Throwable) {
    when (t) {
        is java.net.UnknownHostException -> {
            // Device cannot resolve server address, often no internet
        }
        is java.net.SocketTimeoutException -> {
            // The request took too long, maybe slow network
        }
        is javax.net.ssl.SSLException -> {
            // SSL related problem
        }
        else -> {
            // Some other unexpected problem
        }
    }
}

HTTP errors are delivered to onResponse with a non successful status code. For example, 400 or 422 for bad request, 401 for unauthorized, 403 for forbidden, 404 for not found, 500 for server internal error, or 503 for service unavailable. You handle these by inspecting response.code() and by reading the errorBody if the server sends a useful error message.

Always check response.isSuccessful before trusting response.body(). Never assume that onResponse means success at the business level, it only means the server replied.

Accessing and Parsing Error Bodies

When a server returns an HTTP error code, it often includes a JSON body with details about what went wrong. Retrofit does not automatically convert this error body to your success model. Instead, you can read it as text or parse it into a dedicated error model.

You access the error body like this:

val errorBody = response.errorBody()?.string()
if (errorBody != null) {
    // Parse errorBody JSON manually or with a converter
}

If you want to parse error JSON into a custom data class, you can reuse the converter that Retrofit uses internally. Retrofit provides a responseBodyConverter method on Retrofit instances. This lets you take the ResponseBody and convert it to a typed object.

For example, assume you have an error model:

data class ApiError(
    val code: Int?,
    val message: String?
)

You can convert the error body like this:

fun parseApiError(retrofit: Retrofit, response: Response<*>): ApiError? {
    val errorBody = response.errorBody() ?: return null
    return try {
        val converter =
            retrofit.responseBodyConverter<ApiError>(
                ApiError::class.java,
                arrayOfNulls<Annotation>(0)
            )
        converter.convert(errorBody)
    } catch (e: Exception) {
        null
    }
}

You can then show a user friendly message based on ApiError.message. This is particularly useful when the server validates input and returns detailed reasons for validation failures.

Categorizing Errors for UI Feedback

On the user interface, you should not show raw technical messages like java.net.UnknownHostException. Instead, you typically classify errors into a few categories and map each category to a message or handling strategy.

Common categories are:

Connection problems. The app cannot reach the server, for example airplane mode or no Wi Fi.

Server problems. The server returned a 5xx error, which usually means something is wrong on the server side.

Client errors. The request is invalid, often 4xx codes like 400 or 422. This might be due to bad input or an expired token.

Unexpected errors. Anything that does not fit the other categories, such as parsing errors or unhandled exceptions.

You can create a sealed class to represent these categories in your app logic:

sealed class NetworkError {
    object NoConnection : NetworkError()
    object Timeout : NetworkError()
    object ServerError : NetworkError()
    data class ClientError(val code: Int, val apiError: ApiError?) : NetworkError()
    object Unknown : NetworkError()
}

Then you can map Retrofit responses into this type:

fun mapError(t: Throwable?): NetworkError {
    return when (t) {
        is java.net.UnknownHostException -> NetworkError.NoConnection
        is java.net.SocketTimeoutException -> NetworkError.Timeout
        else -> NetworkError.Unknown
    }
}
fun mapHttpError(
    retrofit: Retrofit,
    response: Response<*>
): NetworkError {
    val code = response.code()
    val apiError = parseApiError(retrofit, response)
    return if (code in 400..499) {
        NetworkError.ClientError(code, apiError)
    } else {
        NetworkError.ServerError
    }
}

Later, your UI layer can decide how to display friendly messages based on which NetworkError it receives.

Using Exceptions with suspend Functions

If you use Retrofit with Kotlin coroutines, Retrofit can generate suspend functions for your API interface instead of using Call<T>. In that case, errors are delivered as exceptions thrown from the suspend call, instead of through onFailure.

A basic pattern looks like this:

suspend fun loadUserProfile(): Result<UserProfile> {
    return try {
        val response = apiService.getUserProfile()
        if (response.isSuccessful) {
            val body = response.body()
            if (body != null) {
                Result.success(body)
            } else {
                Result.failure(IllegalStateException("Empty body"))
            }
        } else {
            Result.failure(HttpException(response))
        }
    } catch (e: Exception) {
        Result.failure(e)
    }
}

Here HttpException is a Retrofit specific exception for non successful HTTP codes. It provides access to the status code and the original Response.

You can detect errors with:

when (val exception = result.exceptionOrNull()) {
    is HttpException -> {
        val code = exception.code()
        val errorBody = exception.response()?.errorBody()?.string()
    }
    is java.net.UnknownHostException -> {
        // No internet
    }
}

This pattern makes it easy to propagate errors upwards and handle them in a ViewModel or repository, while keeping the core networking code simple.

Retrying Failed Requests

Not every failure is permanent. Sometimes a short network glitch or a temporarily busy server causes a request to fail, but trying again after a short delay would succeed. For this reason you may want to retry some failed requests.

For simple manual retries, you can offer a "Try again" button on the UI and just trigger the same Retrofit call again.

To programmatically retry with Call<T>, you must create a new call or clone the existing one, because a Call instance can only be executed once.

Never enqueue the same Call instance multiple times. Use call.clone() to create a fresh instance before retrying.

For example:

fun <T> Call<T>.enqueueWithRetry(
    maxRetries: Int,
    currentRetry: Int = 0,
    callback: Callback<T>
) {
    enqueue(object : Callback<T> {
        override fun onResponse(call: Call<T>, response: Response<T>) {
            callback.onResponse(call, response)
        }
        override fun onFailure(call: Call<T>, t: Throwable) {
            if (currentRetry < maxRetries) {
                call.clone().enqueueWithRetry(maxRetries, currentRetry + 1, callback)
            } else {
                callback.onFailure(call, t)
            }
        }
    })
}

You should be careful with automatic retries. Too many retries can waste battery and data, and can overload the server. Usually, you combine limited retries with user controlled retries through the UI.

Graceful Error Messages and Fallbacks

Retrofit itself only handles transport and conversion. How you inform the user is up to your app. Good error handling tries to keep the app usable and gives clear instructions.

Instead of showing generic alerts like "Error occurred", describe the situation in simple terms. For example, "No internet connection. Please check your network and try again." Instead of crashing when parsing fails, show something like "We are having trouble loading this content. Please try again later."

Sometimes you can provide fallback behavior. If fetching from the network fails, you might show cached data from local storage. In other cases, you might disable certain features until the network is back.

Even when an error occurs, do not block the entire app if it is not necessary. Let the user continue using other sections that do not depend on the failing request.

Logging and Debugging Errors

Effective error handling also includes recording information that helps you debug problems during development and testing. With Retrofit and OkHttp, you can log the HTTP requests and responses for debugging.

For example, using an OkHttp logging interceptor, you can see the request URL, headers, and response codes. When an error occurs, you can log the HTTP status code and the error body.

Even in production, it is useful to collect non sensitive error data, such as the type of error, the API endpoint, and the HTTP status code. This helps you find recurring issues and improve your API and app over time.

Be careful not to log sensitive data like passwords or tokens. When logging response bodies, make sure you are not storing personal information or secrets.

Summary of Retrofit Error Handling

Error handling with Retrofit is about understanding where and how requests can fail, then turning those failures into clear, controlled behavior. You distinguish between network failures in onFailure or thrown exceptions, and HTTP errors in non successful responses. You read and parse error bodies when the server provides more details, and you categorize errors into types that your UI can handle in a user friendly way.

By designing consistent error models, careful retry logic, and clear messages, you can make your app feel robust and reliable, even when the network and server are not.

Views: 1

Comments

Please login to add a comment.

Don't have an account? Register now!