Popular Searches
Popular Course Categories
Popular Courses

Handling Network Errors, Timeouts, and Unsuccessful Responses

Handling Network Errors, Timeouts, and Unsuccessful Responses

Networking and REST API Integration

Handling Network Errors, Timeouts, and Unsuccessful Responses in Android

Course: Android App Development

Topic: Handling Network Errors, Timeouts, and Unsuccessful Responses

Course Link: Android App Development Course

Demo Link: Register for a Free Course Demo

Official Website: JustAcademy


1. Introduction

Modern Android applications frequently communicate with remote servers to retrieve information, authenticate users, submit forms, upload files, and update databases. These operations depend on a reliable network connection and a functioning backend server.

However, network requests do not always succeed. A device may lose internet connectivity, a server may respond too slowly, an API may return an HTTP error status, or the response may contain unexpected data. Without proper error handling, an application can crash, remain stuck on a loading screen, display incorrect information, or frustrate users.

Handling network errors, timeouts, and unsuccessful responses means detecting failures, identifying their causes, displaying meaningful messages, and providing appropriate recovery options.

2. Learning Objectives

  • Understand common Android networking failures.
  • Differentiate network errors from HTTP response errors.
  • Configure connection, read, and write timeouts.
  • Handle unsuccessful responses using Retrofit and Kotlin coroutines.
  • Display useful error messages in the application interface.
  • Implement retry mechanisms without creating duplicate requests.
  • Use structured error handling with repositories and ViewModels.
  • Debug network problems and test failure scenarios.

3. Understanding Network Errors

A network error occurs when an application cannot complete a request because of a connectivity issue, connection failure, timeout, or another communication problem.

Common Network Errors

Error Possible Cause Recommended Handling
No internet connection Wi-Fi or mobile data is unavailable. Inform the user and offer a retry option.
Unknown host DNS cannot resolve the server hostname. Check the hostname, DNS, and connectivity.
Connection refused The server is not accepting connections. Check the backend service and server port.
Connection reset The connection is interrupted unexpectedly. Handle the failure and retry when appropriate.
Connection timeout The server cannot be reached within the configured time. Check connectivity and timeout configuration.
SSL/TLS error The certificate or secure connection cannot be validated. Correct the certificate or TLS configuration.
Unexpected response format The server returns malformed or incompatible JSON. Inspect the response and correct parsing or API contracts.

4. Network Errors vs. Unsuccessful HTTP Responses

One of the most important concepts in Android API development is understanding the difference between a failed network request and an HTTP response that reports failure.

Network Failure

A network failure occurs when the application cannot successfully receive an HTTP response. Examples include no internet connection, DNS failure, or a timeout.

Unsuccessful HTTP Response

An unsuccessful HTTP response means the server returned an HTTP status code indicating that the request did not succeed. The application may still have successfully connected to the server.

Situation Example What the Application Receives
Network failure Phone has no internet. An exception, commonly an IOException or a subclass.
Timeout Server takes too long to respond. Usually a timeout-related IOException.
Client error Invalid request data. HTTP 4xx response.
Server error Backend crashes while processing a request. HTTP 5xx response.
Successful response Request is processed successfully. Usually an HTTP 2xx response.

Important: HTTP error responses such as 404 and 500 are not the same as connectivity exceptions. The way they are handled depends on whether Retrofit returns a Response<T> or a regular body through a suspend function.

5. Understanding Important HTTP Status Codes

Status Code Meaning Typical Application Action
200 OK Request succeeded. Display the returned data.
201 Created A resource was created. Confirm successful creation.
204 No Content Request succeeded without a response body. Update the interface without expecting data.
400 Bad Request The request is invalid. Validate input and explain the problem.
401 Unauthorized Authentication is missing or invalid. Refresh authentication or ask the user to sign in.
403 Forbidden The user is not permitted to perform the operation. Display an access-related message.
404 Not Found The requested resource does not exist. Show a not-found message.
408 Request Timeout The server reports that the request timed out. Offer a retry when safe.
409 Conflict The request conflicts with the current resource state. Refresh data or explain the conflict.
422 Unprocessable Content The request data cannot be processed. Display appropriate validation errors.
429 Too Many Requests The client has exceeded a rate limit. Respect Retry-After when provided.
500 Internal Server Error The backend encountered an error. Display a general server error message.
502 Bad Gateway A gateway received an invalid upstream response. Retry later if appropriate.
503 Service Unavailable The service is unavailable or overloaded. Wait and retry according to server guidance.
504 Gateway Timeout A gateway did not receive a timely upstream response. Retry later if appropriate.

Applications should not assume that every backend uses every status code in exactly the same way. Follow the API's documented response contract.

6. Adding Internet Permission

Before making API requests, declare internet permission in the Android manifest.

<uses-permission android:name="android.permission.INTERNET" />

This permission allows the application to open network connections. It is a normal permission and does not require a runtime permission dialog.

For applications that need to inspect general network connectivity, the following permission may also be declared:

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Checking connectivity can help improve the user experience, but it does not guarantee that a particular server is reachable. The actual request must still handle failures.

7. Configuring Network Timeouts with Retrofit and OkHttp

A timeout limits how long a networking operation can wait. Without suitable limits, requests may take too long and leave users waiting unnecessarily.

Types of Timeouts

  • Connect timeout: Maximum time allowed to establish a connection.
  • Read timeout: Maximum time allowed while waiting for data during a read operation.
  • Write timeout: Maximum time allowed while sending data during a write operation.
  • Call timeout: Maximum total duration for a complete call, including connection setup, request transmission, server waiting, and response reading.

Example: Configuring OkHttp

import java.util.concurrent.TimeUnit
import okhttp3.OkHttpClient

val okHttpClient = OkHttpClient.Builder()
    .connectTimeout(15, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .writeTimeout(30, TimeUnit.SECONDS)
    .callTimeout(45, TimeUnit.SECONDS)
    .build()

The values above are examples, not universal defaults. Choose suitable values based on the API, expected response size, upload requirements, and user experience.

Creating a Retrofit Instance

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

Replace the example base URL with your actual HTTPS API base URL. The base URL used by Retrofit must end with a slash.

8. Defining an API Service

Retrofit converts annotated Kotlin interface methods into HTTP requests. A suspend function can be called from a coroutine, making it suitable for use with a ViewModel and repository.

import retrofit2.http.GET
import retrofit2.http.Path

data class User(
    val id: Int,
    val name: String,
    val email: String
)

interface UserApiService {
    @GET("users/{id}")
    suspend fun getUser(
        @Path("id") userId: Int
    ): User
}

When this function is called, Retrofit requests the specified user. If the response is unsuccessful, Retrofit can throw an HttpException for this return type. Network and timeout failures can produce an IOException.

9. Handling Network Exceptions in Kotlin

Use a try-catch block to handle expected failures around a network request. Catch specific exception types before more general exceptions so that each failure can receive suitable handling.

import java.io.IOException
import kotlinx.coroutines.CancellationException
import retrofit2.HttpException

suspend fun loadUser(userId: Int): User? {
    return try {
        userApiService.getUser(userId)
    } catch (e: CancellationException) {
        // Preserve coroutine cancellation.
        throw e
    } catch (e: HttpException) {
        // The server returned an unsuccessful HTTP status.
        when (e.code()) {
            401 -> {
                // Authentication is required.
            }
            404 -> {
                // User was not found.
            }
            in 500..599 -> {
                // Server-side problem.
            }
            else -> {
                // Other HTTP error.
            }
        }
        null
    } catch (e: IOException) {
        // Connection, DNS, timeout, or other I/O failure.
        null
    }
}

This is an illustrative example. In production code, returning null alone may not communicate enough information to the UI. A typed result is generally more useful.

10. Creating a Result Model for Network Operations

A result model makes success and failure states explicit. Instead of mixing exception handling with UI code, the repository can return a structured result that the ViewModel understands.

sealed interface ApiResult<out T> {
    data class Success<T>(
        val data: T
    ) : ApiResult<T>

    data class HttpError(
        val code: Int,
        val message: String
    ) : ApiResult<Nothing>

    data class NetworkError(
        val message: String
    ) : ApiResult<Nothing>

    data class UnknownError(
        val message: String
    ) : ApiResult<Nothing>
}

Using the Result Model in a Repository

import java.io.IOException
import kotlinx.coroutines.CancellationException
import retrofit2.HttpException

class UserRepository(
    private val api: UserApiService
) {
    suspend fun getUser(userId: Int): ApiResult<User> {
        return try {
            ApiResult.Success(api.getUser(userId))
        } catch (e: CancellationException) {
            throw e
        } catch (e: HttpException) {
            ApiResult.HttpError(
                code = e.code(),
                message = "The server returned an error."
            )
        } catch (e: IOException) {
            ApiResult.NetworkError(
                message = "Unable to connect. Check your internet connection."
            )
        } catch (e: Exception) {
            ApiResult.UnknownError(
                message = "An unexpected error occurred."
            )
        }
    }
}

The repository translates low-level failures into a consistent application-level result. Avoid displaying raw exception messages to users because they may contain technical details that are confusing or unsafe to expose.

11. Displaying Loading, Success, and Error States

Network requests should communicate their current state to the user. A common design uses three primary states:

  • Loading: The request is in progress.
  • Success: Data has been received and can be displayed.
  • Error: The request could not be completed.

Defining a UI State

sealed interface UserUiState {
    data object Loading : UserUiState
    data class Success(val user: User) : UserUiState
    data class Error(val message: String) : UserUiState
}

Handling the Request in a ViewModel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    private val _uiState =
        MutableStateFlow<UserUiState>(UserUiState.Loading)

    val uiState: StateFlow<UserUiState> =
        _uiState.asStateFlow()

    fun loadUser(userId: Int) {
        viewModelScope.launch {
            _uiState.value = UserUiState.Loading

            when (val result = repository.getUser(userId)) {
                is ApiResult.Success -> {
                    _uiState.value =
                        UserUiState.Success(result.data)
                }

                is ApiResult.HttpError -> {
                    _uiState.value =
                        UserUiState.Error(
                            messageForHttpCode(result.code)
                        )
                }

                is ApiResult.NetworkError -> {
                    _uiState.value =
                        UserUiState.Error(
                            "Check your connection and try again."
                        )
                }

                is ApiResult.UnknownError -> {
                    _uiState.value =
                        UserUiState.Error(
                            "Something went wrong. Please try again."
                        )
                }
            }
        }
    }

    private fun messageForHttpCode(code: Int): String {
        return when (code) {
            400 -> "Please check the submitted information."
            401 -> "Your session may have expired. Sign in again."
            403 -> "You do not have permission to perform this action."
            404 -> "The requested information could not be found."
            409 -> "The data has changed. Refresh and try again."
            429 -> "Too many requests. Please wait before trying again."
            in 500..599 -> "The server is temporarily unavailable."
            else -> "The request could not be completed."
        }
    }
}

In a real project, ensure that your ViewModel factory or dependency injection framework provides the repository. Also ensure that error and loading states are collected by the UI lifecycle-aware APIs.

12. Handling Unsuccessful Responses with Retrofit Response<T>

Retrofit can return a Response<T> rather than throwing an HttpException for an unsuccessful HTTP status. This is useful when the application needs to inspect the status code or error body directly.

import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path

interface UserResponseApi {
    @GET("users/{id}")
    suspend fun getUserResponse(
        @Path("id") userId: Int
    ): Response<User>
}

Checking the Response

suspend fun fetchUser(userId: Int) {
    try {
        val response = userResponseApi.getUserResponse(userId)

        if (response.isSuccessful) {
            val user = response.body()

            if (user != null) {
                // Display the returned user.
            } else {
                // Handle an empty or unexpected response body.
            }
        } else {
            val statusCode = response.code()
            val errorBody = response.errorBody()?.string()

            // Log statusCode safely and parse errorBody
            // according to the backend's documented format.
        }
    } catch (e: CancellationException) {
        throw e
    } catch (e: IOException) {
        // Network or timeout failure.
    }
}

Important: An HTTP error response does not automatically enter the catch block when the API method returns Response<T>. Check isSuccessful and inspect the status code. An error body is typically consumed when read, so do not attempt to read the same body repeatedly.

13. Parsing Error Response Bodies

Some APIs return structured error information, such as an error code and message. For example, the backend might return:

{
  "code": "INVALID_INPUT",
  "message": "The email address is invalid."
}

The exact structure depends on the API. Create a Kotlin data class that matches the backend's documented error format.

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

You can parse an error body using the JSON library configured for your application, such as Gson or Kotlin serialization. Handle malformed or empty error bodies gracefully. Never assume every server error contains valid JSON.

14. Implementing Retry Functionality

A retry option allows users to repeat a failed request after correcting the problem or waiting for the service to recover.

Manual Retry

For user-initiated retries, display a button such as Try Again. When clicked, invoke the ViewModel method that performs the request again.

fun retryLoadingUser(userId: Int) {
    loadUser(userId)
}

Connect this function to the retry button in your Android UI. Disable or debounce the button while a request is in progress to reduce accidental duplicate calls.

Automatic Retry with Exponential Backoff

For temporary failures, automatic retry may be appropriate. Exponential backoff increases the wait between attempts so that repeated requests do not overload the backend.

import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import java.io.IOException

suspend fun <T> retryRequest(
    maxAttempts: Int = 3,
    request: suspend () -> T
): T {
    require(maxAttempts > 0)

    var attempt = 0

    while (true) {
        try {
            return request()
        } catch (e: CancellationException) {
            throw e
        } catch (e: IOException) {
            attempt++

            if (attempt >= maxAttempts) {
                throw e
            }

            val delayMillis =
                1_000L * (1L shl (attempt - 1))

            delay(delayMillis)
        }
    }
}

This example retries only I/O failures and waits approximately one second, then two seconds before subsequent attempts. Production implementations should also consider randomized jitter, a maximum delay, cancellation, and application-specific retry policies.

Do not automatically retry every HTTP error. For example, repeating a 400 Bad Request without changing the input will usually fail again. For 429 and selected 503 responses, respect the server's Retry-After guidance where available.

15. Safe Retry Rules for GET, POST, PUT, PATCH, and DELETE

Method Retry Consideration
GET Usually safe to retry because it is intended to retrieve data without changing server state.
HEAD Usually safe to retry because it retrieves response metadata.
PUT Defined as idempotent by HTTP semantics, but the API implementation should still be verified.
DELETE Defined as idempotent by HTTP semantics, but application behavior and response handling still matter.
POST May create duplicate resources or payments if retried after an uncertain outcome.
PATCH May or may not be idempotent depending on the patch operation.

For important write operations, use backend-supported idempotency keys or another duplicate-prevention mechanism. A timeout does not prove that the server failed to process a request; the server may have completed the operation while the response was lost.

16. Managing Loading Indicators and Error Messages

When a request starts, display a progress indicator or another clear loading state. When the request succeeds, display the data. When it fails, stop the loading indicator and show a useful error message.

Recommended User Messages

Situation Example Message
No connection You appear to be offline. Check your connection and try again.
Timeout The request took too long. Please try again.
Authentication issue Your session may have expired. Please sign in again.
Resource not found We couldn't find the requested information.
Server error We're having trouble connecting to the service. Try again later.
Rate limit Too many requests. Please wait a moment before trying again.

Keep error messages understandable and actionable. Do not expose stack traces, access tokens, internal hostnames, database details, or raw server exceptions to end users.

17. Logging and Debugging Network Failures

Useful diagnostic information helps developers determine whether a problem comes from the device, request, server, or response parsing.

  • Record the API endpoint or a sanitized endpoint identifier.
  • Record the HTTP status code for unsuccessful responses.
  • Record request duration and timeout information.
  • Record a correlation or request ID when the backend provides one.
  • Use Android Studio Logcat to inspect exceptions during development.
  • Use an HTTP logging interceptor carefully in development builds.
  • Inspect backend logs to determine whether the request reached the server.

Never log passwords, authentication tokens, payment details, or other sensitive information. Production logging should be sanitized and should follow your application's privacy and security requirements.

18. Common Mistakes and Their Solutions

Mistake Why It Is a Problem Solution
Ignoring exceptions The UI may remain in a loading state. Handle expected failures and update the UI state.
Treating every failure as a network issue HTTP errors require different handling. Separate I/O exceptions from HTTP status errors.
Using unlimited timeouts Requests may wait too long. Configure appropriate timeout limits.
Retrying every request automatically May overload the server or duplicate write operations. Use method-aware retry policies and idempotency protections.
Blocking the main thread Can cause an unresponsive interface or an ANR. Use asynchronous networking with coroutines or callbacks.
Displaying raw exception messages Messages may be confusing or reveal technical details. Map exceptions to safe user-facing messages.
Reading an error body repeatedly Response bodies are generally one-shot streams. Read once and parse the result.
Swallowing coroutine cancellation Unneeded work may continue after the UI no longer needs it. Re-throw CancellationException.
Assuming connectivity guarantees server access A device may be online while the API is unreachable. Handle actual request failures regardless of connectivity checks.

19. Best Practices

  • Use Retrofit with OkHttp or another suitable networking library.
  • Keep networking logic inside a repository or data layer.
  • Use Kotlin coroutines for asynchronous requests.
  • Handle HTTP errors and I/O exceptions separately.
  • Configure connection, read, write, and call timeouts according to the API.
  • Use typed UI states for loading, success, and failure.
  • Offer a retry action when recovery is possible.
  • Respect server rate limits and retry instructions.
  • Avoid retries for invalid requests that require user correction.
  • Protect non-idempotent operations against duplicate submission.
  • Use HTTPS and never disable certificate validation to bypass SSL errors.
  • Test with slow connections, offline mode, server failures, and malformed responses.
  • Use lifecycle-aware state collection and avoid retaining Activity references in long-running operations.

20. Practical Project: Build a Reliable User Details Screen

Create a simple Android screen that retrieves user details from an API and handles common failure scenarios.

Project Requirements

  1. Create a Retrofit API service to retrieve a user by ID.
  2. Configure OkHttp timeout settings.
  3. Create a repository that returns success or error results.
  4. Use a ViewModel to manage loading, success, and error states.
  5. Display user details after a successful response.
  6. Display an appropriate message when the request fails.
  7. Add a Try Again button for recoverable failures.
  8. Prevent multiple simultaneous requests from repeated button clicks.

Testing Scenarios

  • Test with a valid user ID.
  • Test with an ID that does not exist.
  • Turn off Wi-Fi and mobile data before making the request.
  • Simulate a slow server or delay the response.
  • Test server responses such as 401, 404, 429, and 500.
  • Test an empty response body and malformed JSON.
  • Navigate away from the screen while the request is running.
  • Verify that retrying does not create duplicate write operations.

21. Interview Questions and Answers

Q1. What is a network error in Android?

Answer: A network error occurs when an application cannot complete communication with a server. Examples include no internet connection, DNS failure, connection reset, and timeout.

Q2. What is the difference between a network error and an HTTP error?

Answer: A network error generally means the request could not produce a usable HTTP response. An HTTP error means the server returned a response with an unsuccessful status, such as 404 or 500.

Q3. What is a timeout?

Answer: A timeout is a configured time limit for a networking operation. When the operation exceeds that limit, it fails rather than waiting indefinitely.

Q4. What is the difference between connect and read timeouts?

Answer: A connect timeout limits the time required to establish a connection. A read timeout limits waiting for data during a read operation.

Q5. How does Retrofit handle unsuccessful HTTP responses?

Answer: With a suspend function returning a response body type, an unsuccessful HTTP status typically produces an HttpException. With a function returning Response<T>, the application checks isSuccessful and examines the status code and error body.

Q6. Why should network requests not run on the main thread?

Answer: Network operations can take an unpredictable amount of time. Blocking the main thread can freeze the interface and potentially cause an Application Not Responding (ANR) error.

Q7. When should an application retry a failed request?

Answer: Retry when the failure may be temporary and the operation is safe to repeat. Respect server retry instructions and avoid blindly repeating operations that could create duplicate transactions.

Q8. What is exponential backoff?

Answer: Exponential backoff increases the delay between retry attempts. It reduces unnecessary repeated traffic when a server or network is temporarily unavailable.

Q9. Why should CancellationException be re-thrown?

Answer: It preserves structured coroutine cancellation. Treating cancellation as an ordinary failure can interfere with lifecycle-aware cancellation and allow unnecessary work to continue.

Q10. How should error messages be shown to users?

Answer: Use clear, safe messages that explain the problem and suggest an action, such as checking connectivity or trying again later. Keep technical details in sanitized developer logs.

22. Quick Revision Checklist

  • Network errors and HTTP errors are different.
  • Use HTTP status codes to classify unsuccessful server responses.
  • Configure suitable connect, read, write, and call timeouts.
  • Use try-catch around expected network failures.
  • Preserve coroutine cancellation.
  • Return structured results from the repository.
  • Display loading, success, and error states in the UI.
  • Retry only when appropriate and safe.
  • Respect Retry-After and API rate limits.
  • Test offline, timeout, server-error, and malformed-response scenarios.
  • Never expose sensitive data in logs or error messages.

23. Conclusion

Handling network errors, timeouts, and unsuccessful responses is an essential part of building reliable Android applications. A well-designed application distinguishes connectivity failures from HTTP errors, configures appropriate timeout values, manages loading and error states, and provides safe retry options.

By combining Retrofit, OkHttp, Kotlin coroutines, repositories, and ViewModels, developers can build applications that remain understandable and responsive even when network conditions or backend services are unreliable.


Additional Learning Resources

Previous Next
whatsapp