Crash Resiliency & Graceful Degradation

📖 Concept

Crash resiliency means designing your app to recover gracefully from errors, handle unexpected states, and provide degraded functionality instead of crashing.

Crash prevention strategies:

  1. Defensive programming — Null checks, bounds checks, input validation
  2. Sealed types for state — Impossible states become compile errors
  3. Global exception handler — Catch unhandled exceptions for logging before crash
  4. Process death handling — Restore state from SavedStateHandle/Room
  5. Network error handling — Show cached data, retry mechanism, user feedback

Process death handling: Android can kill your app process at any time when in the background. When the user returns, the system recreates the Activity stack but your in-memory data is gone.

Survives process death:           Does NOT survive:
- SavedStateHandle (ViewModel)    - ViewModel state (without SavedState)
- onSaveInstanceState Bundle      - Singletons
- Room database                   - In-memory caches
- DataStore/SharedPreferences     - Static variables
- Files                           - Retrofit/OkHttp state

Graceful degradation tiers:

  1. Full functionality — All features work normally
  2. Degraded — Some features unavailable, core works (offline mode)
  3. Minimal — Show cached data with "offline" banner
  4. Error state — Clear error message with retry option
  5. Crash — Last resort, never silent. Log to Crashlytics.

💻 Code Example

codeTap to expand ⛶
1// Global exception handler — log before crash
2class CrashResilience {
3
4 companion object {
5 fun install(application: Application) {
6 val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
7
8 Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
9 try {
10 // Log crash details for investigation
11 CrashReporting.logFatal(throwable)
12 // Save critical state to disk
13 saveCriticalState(application)
14 } finally {
15 // Let the default handler show crash dialog
16 defaultHandler?.uncaughtException(thread, throwable)
17 }
18 }
19 }
20 }
21}
22
23// Process death-safe ViewModel
24@HiltViewModel
25class FormViewModel @Inject constructor(
26 private val savedStateHandle: SavedStateHandle
27) : ViewModel() {
28 // These survive process death via SavedStateHandle
29 val name = savedStateHandle.getStateFlow("name", "")
30 val email = savedStateHandle.getStateFlow("email", "")
31 val step = savedStateHandle.getStateFlow("step", 0)
32
33 fun updateName(value: String) { savedStateHandle["name"] = value }
34 fun updateEmail(value: String) { savedStateHandle["email"] = value }
35 fun nextStep() { savedStateHandle["step"] = (step.value + 1) }
36}
37
38// Graceful degradation in Repository
39class ProductRepository @Inject constructor(
40 private val api: ProductApi,
41 private val dao: ProductDao,
42 private val connectivity: NetworkMonitor
43) {
44 fun getProducts(): Flow<Resource<List<Product>>> = flow {
45 // Always emit cached data first
46 val cached = dao.getAll().first().map { it.toDomain() }
47 if (cached.isNotEmpty()) {
48 emit(Resource.Success(cached, isStale = true))
49 } else {
50 emit(Resource.Loading)
51 }
52
53 // Try to refresh from network
54 if (connectivity.isConnected.value) {
55 try {
56 val fresh = api.getProducts()
57 dao.upsertAll(fresh.map { it.toEntity() })
58 emit(Resource.Success(fresh.map { it.toDomain() }, isStale = false))
59 } catch (e: Exception) {
60 if (cached.isEmpty()) {
61 emit(Resource.Error(e.message ?: "Failed to load", null))
62 }
63 // If we have cached data, just log the error
64 }
65 } else if (cached.isEmpty()) {
66 emit(Resource.Error("No internet connection", null))
67 }
68 }
69}
70
71sealed interface Resource<out T> {
72 data object Loading : Resource<Nothing>
73 data class Success<T>(val data: T, val isStale: Boolean = false) : Resource<T>
74 data class Error<T>(val message: String, val data: T?) : Resource<T>
75}

🏋️ Practice Exercise

Practice:

  1. Test process death handling: enable "Don't keep activities" in Developer Options and navigate your app
  2. Implement SavedStateHandle for a multi-step form that survives process death
  3. Design a graceful degradation strategy with 3 fallback tiers for your app
  4. Set up a global exception handler that logs to Crashlytics before crashing
  5. Implement a Resource wrapper that handles loading/success/error/stale states

⚠️ Common Mistakes

  • Not testing for process death — use 'Don't keep activities' developer option or adb kill command

  • Relying on ViewModel state surviving process death — ViewModel is recreated, use SavedStateHandle

  • Catching Exception globally and swallowing errors — always log, never suppress silently

  • Not providing offline fallback — if the network is down, show cached data instead of an error screen

  • Showing a generic error message — 'Something went wrong' is not helpful. Be specific and offer actions.

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Crash Resiliency & Graceful Degradation. Login to unlock this feature.