Handling Network Errors And Timeouts In Android Apps
A network request can fail for many reasons: a phone may move between Wi-Fi and mobile data, a server may respond slowly, or a DNS lookup may never complete. Android applications need to treat these situations as normal operating conditions rather than rare exceptions.
This matters for Australian users in particular. A commuter travelling through Sydney or Melbourne can lose connectivity in a tunnel, while someone in regional Queensland or Western Australia may experience longer gaps in coverage. An app that handles connection failures clearly feels reliable, even when the underlying network is unpredictable.
Why Network Requests Fail
Network errors generally fall into three groups: connectivity problems, server-side failures, and client-side mistakes. A device may be offline, a hostname may be unavailable, or an API may return an HTTP status such as 404, 401, or 500. Each case needs a different response.
Timeouts are slightly different from immediate failures. A connection timeout occurs when the app cannot establish a connection within the permitted period. A read timeout happens after the connection is made but the server sends data too slowly. A write timeout can occur while uploading a request body.
Avoid placing network work on Android’s main thread. Blocking the UI thread can produce an NetworkOnMainThreadException, freeze visible controls, and eventually cause an Application Not Responding error. Use Kotlin coroutines, WorkManager, or a suitable networking library for background execution.
Choosing Sensible Timeout Values
Timeout values should reflect the type of request. A quick request for a weather summary might use a shorter limit than a document upload. Extremely long limits make users think the application has stopped responding, while very short limits can reject a perfectly healthy service on a congested mobile network.
OkHttp and Retrofit provide separate connection, read, and write settings. A typical configuration might look like this:
val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
These values are starting points, not universal rules. Test them on home broadband, public Wi-Fi, 4G, 5G, and weak regional connections. Australian users may switch between a household NBN connection and mobile data during the same session, so timeout behaviour should remain predictable across both environments.
Converting Failures Into Useful States
An exception message is useful for developers but rarely helpful to users. Instead of displaying SocketTimeoutException, expose a small set of application states such as loading, success, offline, server error, and unexpected failure. The screen can then show an appropriate message and action.
For example, an offline state might say that an internet connection is unavailable and offer a retry button. A server error can explain that the service is temporarily unavailable. Authentication failures should direct the user towards signing in again rather than asking them to repeatedly retry a request that cannot succeed.
With Retrofit, inspect both transport exceptions and HTTP responses. A response with status code 500 is not the same as an UnknownHostException. Logging the original exception, request type, response code, and elapsed time gives developers enough information to diagnose recurring incidents without exposing technical details in the interface.
Implementing Requests With Coroutines
Kotlin coroutines make asynchronous code easier to read while keeping network operations away from the main thread. A repository can catch expected exceptions and return a result that the ViewModel converts into UI state.
suspend fun loadProfile(): Result<Profile> =
try {
Result.success(api.getProfile())
} catch (error: UnknownHostException) {
Result.failure(NetworkUnavailableException(error))
} catch (error: SocketTimeoutException) {
Result.failure(RequestTimeoutException(error))
} catch (error: IOException) {
Result.failure(NetworkUnavailableException(error))
}
The ViewModel should launch this work in an appropriate lifecycle scope and update the screen only while the related UI is active. A StateFlow or LiveData object can carry loading and error states to an Activity or Fragment. This prevents stale responses from updating a screen after the user has navigated away.
Use Dispatchers.IO when the code performs blocking input/output, although many modern libraries already manage their own threads. For practical Android examples involving application structure and data handling, developers can also review sample mobile projects alongside official Android documentation.
Retrying Without Creating More Problems
Retries can recover from a brief connection drop, but careless retry logic can overload a server and drain a user’s battery or mobile data. Retry only operations that are safe to repeat, such as reading a resource or submitting an idempotent request. Be careful with payments, bookings, and order creation, where a second attempt could create a duplicate transaction.
Exponential backoff increases the delay after each failed attempt. A sequence such as two, four, and eight seconds is more respectful than sending requests continuously. Add a small random delay, known as jitter, so many devices do not retry at exactly the same moment after a service interruption.
Limit the number of attempts and stop when the user leaves the relevant screen. For background synchronisation, WorkManager can apply network constraints and persist work across process restarts. A queued job should also use an idempotency key when the server supports one.
Designing For Offline And Changing Connectivity
Before starting a request, ConnectivityManager can provide a useful indication of whether a network is currently available. It should not be treated as proof that the server is reachable, because a connected Wi-Fi network may have no internet access. The request itself remains the final test.
Cache data that users are likely to revisit, such as account details, articles, or previously loaded lists. Display the cached version with a clear last-updated time, then refresh it when a connection is available. This approach is valuable for people travelling through rural areas or using limited mobile data plans.
Australian apps should also consider privacy obligations under the Privacy Act 1988 and the Australian Privacy Principles. Avoid writing access tokens, personal details, or full request bodies to logs. If failed requests contain sensitive customer information, use encrypted storage and review whether analytics tools are collecting that data.
Practical Error-Handling Checklists
A consistent checklist makes network code easier to review and maintain.
Request handling
- Run network work away from the main thread.
- Configure connection, read, and write timeouts.
- Handle HTTP errors separately from transport exceptions.
- Cancel work when its screen or lifecycle no longer needs it.
When testing, reproduce both instant failures and delayed responses. Android Studio’s emulator controls, proxy tools, and throttled connections can simulate weak networks more reliably than testing only on fast office Wi-Fi.
User experience
- Show a clear loading state without blocking the whole screen.
- Provide a retry action for recoverable failures.
- Preserve cached content when fresh data cannot be loaded.
- Explain when an action may have been submitted already.
A well-designed error flow gives users control and protects the service behind the app. It also helps support teams distinguish a local connectivity issue from an API outage, making troubleshooting faster for customers across Australia.