The most frustrating part of java.io.IOException: grpc failed is how little it tells you. The word gRPC looks precise, but it is usually an implementation detail leaking through the device’s geocoding service. A working network does not guarantee a result, and retrying the same blocking call on the main thread can turn a recoverable lookup failure into a frozen or crashed screen.

What to change immediately

  • On Android 13/API 33 and later, use the asynchronous GeocodeListener overload.

  • On older releases, call the deprecated synchronous overload only from a background dispatcher.

  • Check Geocoder.isPresent() before attempting a lookup.

  • Validate latitude, longitude, and result-count inputs.

  • Treat an empty address list as “no match,” not as an exception.

  • Show coordinates or a retry action when no human-readable address is available.

  • Never make safety-critical, legal, emergency, or compliance decisions from geocoder text.

A lifecycle-friendly Kotlin implementation

GeocoderRepository.ktkotlin
import android.content.Context
import android.location.Address
import android.location.Geocoder
import android.os.Build
import java.io.IOException
import java.util.Locale
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
 
class GeocoderUnavailableException : IOException(
    "No geocoder implementation is installed on this device"
)
 
class AndroidGeocoderRepository(
    context: Context,
    locale: Locale = Locale.getDefault(),
) {
    private val appContext = context.applicationContext
    private val geocoder = Geocoder(appContext, locale)
 
    suspend fun reverseGeocode(
        latitude: Double,
        longitude: Double,
    ): Result<Address?> {
        require(latitude in -90.0..90.0) { "Invalid latitude" }
        require(longitude in -180.0..180.0) { "Invalid longitude" }
 
        if (!Geocoder.isPresent()) {
            return Result.failure(GeocoderUnavailableException())
        }
 
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            reverseGeocodeAsync(latitude, longitude)
        } else {
            reverseGeocodeLegacy(latitude, longitude)
        }
    }
 
    private suspend fun reverseGeocodeAsync(
        latitude: Double,
        longitude: Double,
    ): Result<Address?> = suspendCancellableCoroutine { continuation ->
        geocoder.getFromLocation(
            latitude,
            longitude,
            1,
            object : Geocoder.GeocodeListener {
                override fun onGeocode(addresses: MutableList<Address>) {
                    if (continuation.isActive) {
                        continuation.resume(Result.success(addresses.firstOrNull()))
                    }
                }
 
                override fun onError(errorMessage: String?) {
                    if (continuation.isActive) {
                        continuation.resume(
                            Result.failure(
                                IOException(errorMessage ?: "Geocoding failed")
                            )
                        )
                    }
                }
            },
        )
    }
 
    @Suppress("DEPRECATION")
    private suspend fun reverseGeocodeLegacy(
        latitude: Double,
        longitude: Double,
    ): Result<Address?> = withContext(Dispatchers.IO) {
        runCatching {
            geocoder.getFromLocation(latitude, longitude, 1)
                ?.firstOrNull()
        }
    }
}

The API-level split prevents UI blocking

  • context.applicationContext prevents the repository from retaining an Activity.

  • Coordinate require checks fail locally with IllegalArgumentException before invoking the service.

  • Geocoder.isPresent() reports whether an implementation exists, not whether the next request will succeed.

  • The API 33 overload completes through exactly one listener callback; error text may still be null or unhelpful.

  • suspendCancellableCoroutine bridges the callback into a suspend function and ignores a late callback after caller cancellation.

  • The legacy overload may block for a long time, so withContext(Dispatchers.IO) keeps it away from the main thread.

  • Result<Address?> distinguishes failure from a successful lookup with no matching address.

Call it from a ViewModel

AddressViewModel.ktkotlin
data class AddressUiState(
    val loading: Boolean = false,
    val label: String? = null,
    val message: String? = null,
)
 
class AddressViewModel(
    private val geocoder: AndroidGeocoderRepository,
) : ViewModel() {
    private val _state = MutableStateFlow(AddressUiState())
    val state: StateFlow<AddressUiState> = _state.asStateFlow()
 
    fun resolve(location: Location) {
        viewModelScope.launch {
            _state.value = AddressUiState(loading = true)
 
            _state.value = geocoder.reverseGeocode(
                location.latitude, location.longitude
            ).fold(
                onSuccess = { address ->
                    if (address == null) {
                        AddressUiState(message = "No address found for this location")
                    } else {
                        AddressUiState(label = address.getAddressLine(0))
                    }
                },
                onFailure = { error ->
                    Log.w("AddressLookup", "Reverse geocoding failed", error)
                    AddressUiState(
                        message = "Address unavailable. You can retry."
                    )
                },
            )
        }
    }
}

The screen receives state, not exceptions

  • viewModelScope cancels delivery when the ViewModel is cleared and survives normal configuration changes.

  • fold forces separate handling for service failure and a successful empty result.

  • The user sees a stable, non-technical message while logs retain the exception for diagnosis.

  • Do not show raw backend strings such as grpc failed as instructions to users.

  • getAddressLine(0) may be absent or formatted differently by locale; real UI should assemble or fall back from available address fields.

  • If coordinates are useful, preserve them in state so the feature still works without an address label.

Getting a location and geocoding it are separate operations

FusedLocationProviderClient.lastLocation can return null or a stale cached location. For a user-triggered “use my current location” action, getCurrentLocation() is often a better fit when freshness matters. Either way, obtain runtime foreground-location permission in context, then pass the resulting coordinates to the geocoder. The geocoder failure does not mean the location request failed.

  • Approximate permission on Android 12+ can produce coarse coordinates; the resulting address may be less specific.

  • Record the location timestamp and accuracy when freshness affects product behavior.

  • Handle location settings disabled, permission denied, task failure, and null location before geocoding.

  • Do not request background location merely to solve an address-label feature.

  • A latitude/longitude already supplied by the user or server does not require acquiring device location.

Why a network check is not a fix

A validated network is useful telemetry and can help decide whether to offer an immediate retry, but it cannot prove the selected geocoder backend is reachable or healthy. Conversely, some implementations may answer from local data. Make the lookup, handle its documented result, and use connectivity only as supporting context.

A restrained retry policy

  1. Retry only after a transient failure, not after invalid coordinates or a missing implementation.

  2. Wait with exponential backoff and jitter instead of retrying immediately in a tight loop.

  3. Cap attempts—typically one user-visible retry or a small background limit.

  4. Cancel or supersede a request when the user selects another location.

  5. Cache successful coarse results for a product-appropriate duration and coordinate precision.

  6. Offer manual address entry or coordinates when the service remains unavailable.

Diagnose the device, not just the exception text

Android development workstationadb
adb shell getprop ro.build.version.sdk
adb shell getprop ro.product.manufacturer
adb shell getprop ro.product.model
adb logcat -c
adb logcat AndroidRuntime:E AddressLookup:W Geocoder:* *:S
33
Example Manufacturer
Example Device
--------- beginning of main

Capture reproducible context without location leakage

  • Record API level, manufacturer/model, build fingerprint, locale, app version, and whether Geocoder.isPresent() returned true.

  • The filtered log captures the app tag and relevant platform messages; tag availability varies by build.

  • Do not publish precise user coordinates, street addresses, auth tokens, or unrelated logcat data.

  • Compare a physical device, an emulator image with appropriate services, another network, and another locale.

  • Test an obviously addressable coordinate separately from the user’s input to distinguish data coverage from service availability.

Failure map

  • `Geocoder.isPresent()` is false: use a different provider or manual entry; retrying the framework service cannot add an implementation.

  • `onGeocode` returns an empty list: no match was found; display coordinates or ask for a more suitable point.

  • `onError` or legacy `IOException`: treat it as service/transport failure, log context, and offer a bounded retry.

  • `IllegalArgumentException`: latitude or longitude is outside its documented range; fix input validation.

  • ANR or frozen screen on older Android: the synchronous overload ran on the main thread; move it to an IO dispatcher.

  • Address is wrong or incomplete: geocoder results are best effort; allow user confirmation/correction.

  • Only one device image fails: verify that image has a geocoder backend and required system services; device variation is expected.

  • Crash after screen navigation: scope work to ViewModel/lifecycle state and ignore stale callbacks.

Testing strategy

  • Wrap platform geocoding behind an interface so unit tests can return an address, null, or an exception deterministically.

  • Test invalid coordinate boundaries without calling Android services.

  • Instrument API 33+ and at least one pre-33 device/emulator because the execution paths differ.

  • Test airplane mode, intermittent connectivity, unavailable backend, empty response, cancellation, rotation, and rapid repeated selections.

  • Avoid tests that assert a specific live address from a public service; backend data and formatting change.

  • Track success, empty, error, latency, API level, and coarse device family using privacy-safe telemetry.

When to use another geocoding provider

Choose a documented server or commercial geocoding provider when you need predictable quotas, consistent cross-platform formatting, support commitments, or place identifiers. Keep API keys out of the APK when the provider expects confidential credentials, comply with attribution and storage terms, minimize precise-location retention, and obtain appropriate user disclosure and consent.

Official Android references