On this page
The core of your app should never depend on an AI call succeeding. Design every AI feature with a plan for when the model is slow, refuses, errors or isn't available, and make that plan visible to users in a helpful way.
Failure types
| Failure | Typical signal | Good response |
|---|---|---|
| Offline / network error | IOException | On-device tier, queue, or offline message |
| Rate limited | 429 | Backoff and retry once; then explain |
| Quota exhausted | Quota error / your backend's code | Explain limit; offer non-AI path |
| Slow response | Timeout | Cancel, offer retry or smaller model |
| Safety block / refusal | Blocked finish reason | Neutral message; let user rephrase |
| Invalid output | JSON parse / validation fails | Retry once; then fallback |
| Unsupported device | Feature status UNAVAILABLE | Cloud tier or hide feature |
| Model deprecated | 404 / model not found | Remote-config model name; alerting |
A fallback chain
Implementing it
sealed interface AiOutcome<out T> {
data class Ok<T>(val value: T, val source: String) : AiOutcome<T>
data class Unavailable(val reason: Reason) : AiOutcome<Nothing>
enum class Reason { OFFLINE, LIMITED, BLOCKED, FAILED }
}
class ResilientSummarizer(
private val primary: Summarizer, // e.g. cloud Flash
private val secondary: Summarizer?, // e.g. on-device, or a lighter model
private val cache: SummaryCache,
private val flags: AiFlags, // remote config kill switch, model names
) {
suspend fun summarize(key: String, text: String): AiOutcome<String> {
if (!flags.summariesEnabled) return AiOutcome.Unavailable(AiOutcome.Reason.FAILED)
cache.get(key)?.let { return AiOutcome.Ok(it, "cache") }
val first = attempt(primary, text, retries = 1)
if (first is AttemptResult.Success) return first.save(key, "primary")
if (first is AttemptResult.Blocked) return AiOutcome.Unavailable(AiOutcome.Reason.BLOCKED)
secondary?.let { s ->
val second = attempt(s, text, retries = 0)
if (second is AttemptResult.Success) return second.save(key, "secondary")
}
return AiOutcome.Unavailable(if (first is AttemptResult.Offline) AiOutcome.Reason.OFFLINE else AiOutcome.Reason.LIMITED)
}
private suspend fun attempt(s: Summarizer, text: String, retries: Int): AttemptResult {
repeat(retries + 1) { i ->
val r = try {
withTimeout(15_000) { AttemptResult.Success(s.summarize(text)) }
} catch (e: TimeoutCancellationException) { AttemptResult.Failed
} catch (e: IOException) { AttemptResult.Offline
} catch (e: BlockedException) { return AttemptResult.Blocked
} catch (e: RateLimitedException) { AttemptResult.Failed }
if (r is AttemptResult.Success) return r
if (i < retries) delay(1_000L * (i + 1) + Random.nextLong(250)) // backoff with jitter
}
return AttemptResult.Failed
}
private suspend fun AttemptResult.Success.save(key: String, source: String): AiOutcome<String> {
cache.put(key, value); return AiOutcome.Ok(value, source)
}
}
// AttemptResult (Success/Failed/Offline/Blocked) and the exceptions map from your SDK's errors.Kill switches and remote config
- Put feature enablement and model names in Remote Config, so you can disable a feature or switch models without an app release.
- Use per-feature flags, so one broken feature doesn't take down others.
- Set backend-side circuit breakers to stop calling a failing provider for a while.
Failure UX
- Keep the non-AI path first-class: manual editing, regular search, templates.
- Be specific: "You're offline. Summaries need internet on this device." is better than "Something went wrong".
- Preserve user input on failure, and keep partial streamed results.
- Don't blame the user for safety blocks. Suggest rephrasing neutrally.
Frequently asked questions
Should I fall back from one provider to another?
It can improve availability, but it adds complexity, different outputs and privacy implications (another provider's terms). Do it on the backend, if at all, and disclose it.
How many retries?
Usually one, with backoff, for transient errors. More retries add latency and cost, and can worsen rate limiting.
Conclusion
Treat AI as an optional enhancement with a plan B: classify failures, use timeouts and limited retries, a fallback chain (secondary tier, cache, non-AI), kill switches via remote config, and clear, input-preserving failure UX.
Next step: design trustworthy AI interfaces in Responsible AI User Experience.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Every AI feature still runs inside an Android app that has to be fast, reliable and well structured. Strengthen the Kotlin, Jetpack Compose and architecture skills that AI integrations depend on, then apply these AI concepts with confidence.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.