錯誤處理:例外優於錯誤碼 (Error Handling)

★★★★Draft

FromClean Code

📚 From the Books

錯誤處理:例外優於錯誤碼 (Error Handling)Functions

錯誤處理很重要,但如果它讓主要邏輯變得模糊不清,那就是錯誤的做法。整潔的程式碼應該把「處理錯誤的邏輯」和「正常的業務邏輯」分成兩件事——用例外取代錯誤碼,讓主流程讀起來仍像一段乾淨的故事,而不是一連串 if (err != OK) 的巢狀檢查。

🧠 Why It's a Problem

NOTE

這頁講的是 Clean Code 的立場:用例外、特例物件與空集合,把錯誤處理寫得乾淨。另有一條互補但不同的思路——把錯誤設計到不存在(Ousterhout),主張從介面設計上根本減少會拋出的例外數量。兩者角度不同,可對照參看。

⚖️ Case Study

前 — 錯誤碼把主流程壓成巢狀檢查
// 每個回傳值都可能是錯誤碼,呼叫端被迫層層檢查
function sendShutdown(deviceId) {
  const handle = getHandle(deviceId);
  if (handle !== DEVICE_HANDLE.INVALID) {
    const record = retrieveDeviceRecord(handle);
    if (record.status !== DEVICE_SUSPENDED) {
      pauseDevice(handle);
      clearDeviceWorkQueue(handle);
      closeDevice(handle);
    } else {
      logger.error("Device suspended. Unable to shut down");
    }
  } else {
    logger.error("Invalid handle for: " + deviceId);
  }
}

正事(暫停、清佇列、關閉)被埋在錯誤檢查的縮排裡,讀者得撥開一層層 if/else 才找得到主流程。

後 — 例外把錯誤處理抽離,主邏輯浮上來
// 主流程:只講「做什麼」,一眼讀完
function sendShutdown(deviceId) {
  try {
    tryToShutDown(deviceId);
  } catch (e) {
    logger.error(e.message);
  }
}

// 正事獨立成一件事,該失敗就拋
function tryToShutDown(deviceId) {
  const handle = getHandle(deviceId);
  const record = retrieveDeviceRecord(handle);
  pauseDevice(handle);
  clearDeviceWorkQueue(handle);
  closeDevice(handle);
}

錯誤處理與正常邏輯各自成段:tryToShutDown 只管關機的三步,catch 只管出錯後的收拾。這正是「只做一件事」——把錯誤處理當成獨立的關注點。

TIP

拋例外時要帶上情境:不要只丟一個 stack trace,訊息裡應包含「失敗的操作意圖」與「失敗時的相關數據」,讓維護者能判斷錯在哪、為什麼錯。

在封裝第三方 API 時,還要依呼叫端的需求定義例外類別:別把底層的 SocketTimeoutConnectionRefused 一股腦拋給上層,而是包裝(wrap)成對呼叫端有意義的通用例外(例如 PortDeviceFailure)。這既簡化了呼叫端要依賴的例外種類,也讓日後替換底層 library 更容易。

前 — 回傳 null,呼叫端布滿防禦性檢查
// 找不到就回 null,並把 null 傳給下游
function getMeals(employeeId) {
  return mealRepository.find(employeeId); // 可能是 null
}

let total = 0;
const meal = getMeals(employeeId);
// 每個呼叫端都得記得檢查——漏一個就爆
if (meal !== null) {
  total += meal.getTotal();
} else {
  total += getPerDiemDefault();
}

只要有一個呼叫端忘了那個 if (meal !== null),執行期就會炸。null 把「處理缺席」這件事外包給了每一個呼叫端。

後 — 特例物件 / 空集合,讓正常流程順下去
// 找不到就回一個「特例物件」,它自己知道預設值
function getMeals(employeeId) {
  return mealRepository.find(employeeId) ?? new PerDiemMeal();
}

// 呼叫端不必知道有沒有例外,流程一路順下去
const total = getMeals(employeeId).getTotal();

// 回傳集合時,永遠回空陣列而非 null
function getEmployees(dept) {
  const found = employeeRepository.findByDept(dept);
  return found ?? []; // 呼叫端可以直接 for...of,不需先判 null
}

特例模式(Special Case Pattern) 把「缺席」封裝成一個行為正常的物件,用空集合取代 null 回傳——呼叫端不再需要特判,主流程也不再被 null 檢查切碎。

WARNING

如果程式碼裡布滿 if (item != null),通常代表 API 設計有問題,或這段程式的錯誤處理規劃不足——別急著再補一個 null 檢查,該回頭看介面。

🔑 Takeaways

✍️ My Notes

No notes yet — jot your takeaways or Q&A here.

📖 Further Reading