【Android 架構系列 06】錯誤處理 (Error Handling):別讓 AI 吞掉你的 Exception
01. 前言:AI 的「鴕鳥心態」
這是系列文的第六篇。
在前幾篇,我們處理了資料的獲取(Data Layer)與同步(Offline-First)。現在,我們要來面對開發中最令人頭痛,也是 AI 最容易「搞砸」的部分——錯誤處理 (Error Handling)。
AI 有一個壞習慣:它非常害怕程式崩潰。所以當你叫它處理錯誤時,它往往會寫出一個空的 try-catch,把錯誤吞掉。這導致 App 出錯時靜悄悄,用戶以為 App 當機了,開發者也查不到 Log。
這篇將介紹 ROP (Result Oriented Programming,結果導向程式設計) 的概念,教你如何建立一套全域的錯誤攔截機制,別讓 AI 吞掉你的 Exception。
當你要求 AI:「幫我處理 API 的錯誤,不要讓 App 閃退。」
AI 為了達成「不閃退」這個目標,通常會交出這樣的代碼:
// ❌ AI 典型的鴕鳥式寫法
fun getNews() {
viewModelScope.launch {
try {
val data = api.fetchNews()
_uiState.value = NewsUiState.Success(data)
} catch (e: Exception) {
// 😱 災難:錯誤被吞掉了!
// 只有一行 Log,用戶看到的是「無盡的 Loading」
e.printStackTrace()
// 或者隨便塞一個空 list
_uiState.value = NewsUiState.Success(emptyList())
}
}
}
這種寫法在 Production 環境是致命的。
- 使用者困惑: 畫面一直在轉圈圈,或者顯示「無資料」,使用者不知道是網路斷了還是真的沒資料。
- 除錯困難: 如果這是 HTTP 401 (Token 過期),你應該要導向登入頁,而不是顯示空列表。
AI 傾向於寫出這種代碼,因為它最簡單,且符合「不 Crash」的指令。我們必須改變 AI 的思維模式:錯誤不是意外,錯誤是資料的一部分。
02. 核心觀念:ROP (鐵道導向程式設計)
Railway Oriented Programming (ROP) 是一個很棒的比喻。
想像你的程式邏輯是一條鐵軌:
- Success Track (快樂路徑): 一路暢通,資料順利轉換。
- Failure Track (失敗路徑): 一旦發生錯誤,就像火車切換軌道,進入錯誤處理流程,而不是直接脫軌(Crash)。
在 Kotlin 中,我們使用 Result<T> 來實作這個概念。我們要求 AI:不要拋出 Exception,而是回傳一個包含錯誤的 Result 物件。
03. 實戰演練:建立全域錯誤處理機制
我們要建立一套標準,讓 AI 在寫 Repository 和 ViewModel 時,只要套用公式就好。
步驟 1:定義領域錯誤 (Domain Error)
不要直接把 HttpException 或 IOException 丟給 UI 層。UI 層不應該知道你是用 Retrofit 還是 GraphQL。
我們需要叫 AI 定義一個「App 聽得懂」的錯誤分類。
Prompt:
"請定義一個 AppError sealed interface。 包含:NetworkError (無網路), ServerError (5xx), AuthError (401), UnknownError。"
// domain/model/AppError.kt
sealed interface AppError {
data object NetworkError : AppError
data object AuthError : AppError
data class ServerError(val code: Int) : AppError
data class UnknownError(val message: String) : AppError
}
// 擴充 Result 來支援轉型
fun Throwable.toAppError(): AppError {
return when (this) {
is IOException -> AppError.NetworkError
is HttpException -> {
when (this.code()) {
401 -> AppError.AuthError
in 500..599 -> AppError.ServerError(this.code())
else -> AppError.UnknownError("HTTP ${this.code()}")
}
}
else -> AppError.UnknownError(this.message ?: "Unknown")
}
}
步驟 2:Repository 的 ROP 實作
在 Data Layer,我們要強制 AI 使用 runCatching,並立刻將 Exception 轉換為 Result<AppError>。
Prompt:
"修改 NewsRepository。 使用 runCatching 包裹 API 呼叫。 在 onFailure 區塊中,使用 toAppError() 將 Exception 轉換為我們定義的錯誤型別。 回傳型別應為 Result<List<Article>>。"
(註:Kotlin 標準庫的 Result 雖然好用,但泛型限制較多,實務上也可以使用 Arrow 庫的 Either 或自定義 NetworkResult。這裡為了簡單演示,我們假設使用標準 Result)
// data/repository/NewsRepositoryImpl.kt
class NewsRepositoryImpl @Inject constructor(api: NewsApi) : NewsRepository {
override suspend fun getNews(): Result<List<Article>> {
// AI 寫出的 ROP 風格代碼
return runCatching {
val response = api.fetchNews()
response.articles.map { it.toDomain() }
}.fold(
onSuccess = { Result.success(it) },
onFailure = { Result.failure(Exception(it.toAppError().toString())) }
// 實務上建議自定義 Result 類別來攜帶 AppError
)
}
}
更進階的 Prompt:自定義 Result Wrapper
為了讓錯誤處理更優雅,通常我們會叫 AI 寫一個自定義的 Wrapper。
Prompt:
"請建立一個 NetworkResult<T> sealed class。包含 Success(data: T) 和 Error(error: AppError)。"
// common/NetworkResult.kt
sealed class NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error(val error: AppError) : NetworkResult<Nothing>()
}
// Repository 變得很乾淨
override suspend fun getNews(): NetworkResult<List<Article>> {
return try {
val response = api.fetchNews()
NetworkResult.Success(response.articles.map { it.toDomain() })
} catch (e: Exception) {
NetworkResult.Error(e.toAppError()) // 錯誤被轉化為資料
}
}
04. ViewModel 的消費:優雅的錯誤提示
現在,錯誤已經變成了資料 (NetworkResult.Error),流向了 ViewModel。AI 在處理時就不能假裝沒看到了。
Prompt:
"在 ViewModel 中處理 NetworkResult。 如果是 Success,更新 uiState 為成功。 如果是 Error,判斷錯誤類型:
- AuthError -> 發送登出事件。
- 其他錯誤 -> 更新 uiState 為 Error 並顯示訊息。"
// presentation/NewsViewModel.kt
fun fetchNews() {
viewModelScope.launch {
_uiState.value = NewsUiState.Loading
when (val result = repository.getNews()) {
is NetworkResult.Success -> {
_uiState.value = NewsUiState.Success(result.data)
}
is NetworkResult.Error -> {
// AI 被迫處理每一種錯誤情況
val msg = when (val error = result.error) {
is AppError.NetworkError -> "請檢查網路連線"
is AppError.AuthError -> {
_oneTimeEvent.emit(NewsEvent.NavigateToLogin)
"登入已過期"
}
is AppError.ServerError -> "伺服器維修中 (${error.code})"
is AppError.UnknownError -> "發生錯誤:${error.message}"
}
_uiState.value = NewsUiState.Error(msg)
}
}
}
}
05. 結論:讓錯誤現形
透過 ROP 機制,我們達成了一個重要的目標:Explicitness (明確性)。
- Repository 層: 強制將 Exception 轉為 AppError,不讓底層細節洩漏。
- ViewModel 層: 強制透過
when表達式處理 Success/Error 分支,AI 沒辦法只寫 Happy Path。
這樣一來,AI 再也無法當鴕鳥。它必須面對錯誤,並且依照我們制定的規則(例如 AuthError 就登出)來處理。
解決了資料流與錯誤處理,我們的後端邏輯已經非常穩固。接下來,我們要回到前端,面對那個變幻莫測的 UI 層。下一篇,我們將探討如何用 Sealed Interface 來終結 ViewModel 裡的變數大爆炸。
下集預告:【Part 7】狀態的藝術:Presentation Layer 設計——用 Sealed Interface 終結變數大爆炸

關於作者
Ken Huang
熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。
最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。
這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。
系列文章目錄
- 【Android 架構系列 04】核心防禦:Domain 與 Data Layer 設計——防止 AI 污染業務邏輯
- 【Android 架構系列 05】離線優先 (Offline-First):當 AI 遇上快取——如何設計 Single Source of Truth 避免資料打架
- 【Android 架構系列 06】錯誤處理 (Error Handling):別讓 AI 吞掉你的 Exception (本文)
- 【Android 架構系列 07】狀態的藝術:Presentation Layer 設計——用 Sealed Interface 終結變數大爆炸
- 【Android 架構系列 08】現代化 UI (Compose):打造「笨」元件——利用 Preview 與 Slot API 讓 AI 生成完美的無狀態 UI