本文 AI 產出,尚未審核

TypeScript

單元:錯誤處理與例外(Error Handling)

主題:try / catch 的錯誤型別


簡介

在 JavaScript 與 TypeScript 中,例外(exception) 是程式執行期間最常見的非同步或同步錯誤訊號。若不加以捕捉,錯誤會直接冒泡至最上層,導致應用程式崩潰或使用者體驗不佳。
TypeScript 在編譯階段提供了靜態型別檢查的能力,卻在 try / catch 區塊內部的錯誤型別上仍保留了 JavaScript 的彈性——catch 參數的型別預設為 any。這意味著我們可以拋出 字串、數字、Error 物件,甚至是自訂的類別,而 TypeScript 並不會直接警告。

對於 初學者 來說,了解 catch 取得的錯誤究竟是什麼型別、如何正確地斷言(type‑assert)保護(type‑guard),是寫出可維護、可預測程式碼的關鍵;對 中級開發者 則是提升程式碼品質、避免隱藏錯誤的必備技巧。本文將深入探討 try / catch 中錯誤型別的運作機制、常見陷阱與最佳實踐,並提供多個實用範例,協助你在 TypeScript 專案中安全、有效地處理例外。


核心概念

1. try / catch 的基本語法

try {
  // 可能拋出例外的程式碼
} catch (error) {
  // 處理例外
}
  • try:放置可能拋出例外的程式碼。
  • catch:捕捉例外,error 參數的型別在 TypeScript 中預設為 any,因此編譯器不會對其屬性做型別檢查。

⚠️ 注意:在 catch 區塊內直接使用 error.message 會產生 Property 'message' does not exist on type 'any' 的警告,除非你先做型別斷言或型別守衛。


2. 為什麼 catch 參數是 any

JavaScript 本身允許 任意值throw,例如:

throw "Oops!";          // 字串
throw 404;              // 數字
throw new Error("..."); // Error 物件
throw { code: 1 };      // 任意物件

為了與原生行為保持相容,TypeScript 把 catch 參數設為 any,讓開發者自行決定要如何處理不同型別的錯誤。

建議:在 TypeScript 專案中,盡量 只拋出 Error 或其子類別,這樣可以讓型別系統更容易推斷,減少後續的型別守衛工作。


3. 使用 unknown 取代 any(TS 4.0+)

從 TypeScript 4.0 開始,catch 參數的預設型別已改為 unknown(若 --useUnknownInCatchVariables 編譯選項開啟)。unknownany 的差別在於:

特性 any unknown
直接存取屬性 ✅(不會報錯) ❌(需要型別斷言或守衛)
直接呼叫方法
可賦值給其他類型 ✅(任意) ❌(只能賦值給 anyunknown

使用 unknown 能夠 強迫開發者在使用錯誤前先檢查型別,大幅降低因錯誤型別不符而產生的執行時例外。

try {
  // ...
} catch (e: unknown) {
  // 必須先判斷 e 的型別才能使用
}

4. 常見的型別守衛(Type Guard)

以下提供幾種在 catch 區塊內檢查錯誤型別的技巧:

4.1. instanceof Error

catch (e: unknown) {
  if (e instanceof Error) {
    console.error('Error message:', e.message);
  } else {
    console.error('Non‑Error thrown:', e);
  }
}
  • instanceof 只能檢查 原型鏈,適用於自訂的錯誤類別(只要繼承自 Error)。

4.2. 自訂型別守衛

function isHttpError(obj: any): obj is HttpError {
  return obj && typeof obj.status === 'number' && typeof obj.body === 'string';
}
  • 使用 obj is HttpError 讓 TypeScript 在 if (isHttpError(e)) 內部自動推斷 eHttpError

4.3. typeofArray.isArray

catch (e: unknown) {
  if (typeof e === 'string') {
    console.warn('String error:', e);
  } else if (Array.isArray(e)) {
    console.warn('Array error:', e);
  }
}

5. 自訂錯誤類別

自訂錯誤類別能讓錯誤資訊更具語意,且在 catch 時更容易做型別判斷。

class ValidationError extends Error {
  constructor(public readonly field: string, message: string) {
    super(message);
    this.name = 'ValidationError';
    // 為了讓 stack trace 正確,必須手動設定 prototype
    Object.setPrototypeOf(this, ValidationError.prototype);
  }
}

使用方式:

function validateUser(name: string) {
  if (name.length === 0) {
    throw new ValidationError('name', '使用者名稱不能為空');
  }
}

catch 中:

catch (e: unknown) {
  if (e instanceof ValidationError) {
    console.error(`欄位 ${e.field} 錯誤:${e.message}`);
  } else if (e instanceof Error) {
    console.error(e.message);
  }
}

6. 範例彙總

以下提供 5 個實用範例,展示不同錯誤型別的拋出與捕捉方式,並說明每個範例的重點。

範例 1️⃣:僅拋出字串(不建議)

function fetchData() {
  // 假設 API 回傳錯誤代碼
  const status = 500;
  if (status !== 200) {
    throw `Server error: ${status}`; // ← 拋出字串
  }
}

try {
  fetchData();
} catch (e: unknown) {
  // 必須自行判斷型別
  if (typeof e === 'string') {
    console.error('字串型別錯誤:', e);
  }
}

重點:拋出字串會讓錯誤資訊缺乏堆疊追蹤(stack trace),且在 catch 時必須額外檢查型別,降低可讀性。


範例 2️⃣:拋出原生 Error

function readFile(path: string) {
  if (!path) {
    throw new Error('檔案路徑不可為空');
  }
  // ... 讀檔邏輯
}

try {
  readFile('');
} catch (e: unknown) {
  if (e instanceof Error) {
    console.error('Error 名稱:', e.name);
    console.error('Error 訊息:', e.message);
    console.error('Stack trace:', e.stack);
  }
}

重點Error 內建 namemessagestack,適合大多數錯誤情境,且 instanceof 可直接辨識。


範例 3️⃣:自訂錯誤類別 HttpError

class HttpError extends Error {
  constructor(public readonly status: number, public readonly url: string, message: string) {
    super(message);
    this.name = 'HttpError';
    Object.setPrototypeOf(this, HttpError.prototype);
  }
}

function request(url: string) {
  // 模擬 404
  const status = 404;
  if (status !== 200) {
    throw new HttpError(status, url, `無法取得資源 (${status})`);
  }
}

try {
  request('https://example.com/api');
} catch (e: unknown) {
  if (e instanceof HttpError) {
    console.error(`HTTP ${e.status} 錯誤,URL: ${e.url}`);
  } else if (e instanceof Error) {
    console.error(e.message);
  }
}

重點:自訂錯誤可攜帶額外資訊(如 statusurl),在 catch 時能精準判斷與回報。


範例 4️⃣:使用 unknown + 型別守衛

type ApiError = { code: number; detail: string };

function isApiError(obj: any): obj is ApiError {
  return typeof obj === 'object' && obj !== null && 'code' in obj && 'detail' in obj;
}

function callApi() {
  // 模擬拋出自訂結構的錯誤
  throw { code: 1234, detail: '授權失敗' };
}

try {
  callApi();
} catch (e: unknown) {
  if (isApiError(e)) {
    console.error(`API 錯誤代碼 ${e.code}:${e.detail}`);
  } else if (e instanceof Error) {
    console.error(e.message);
  } else {
    console.error('未知錯誤類型', e);
  }
}

重點unknown 迫使開發者寫型別守衛,提升程式安全性。


範例 5️⃣:結合 Promiseasync/await 的錯誤型別

async function getUser(id: number): Promise<{ name: string }> {
  if (id <= 0) {
    throw new ValidationError('id', '使用者 ID 必須為正數');
  }
  // 假設 fetch 失敗會回傳 HttpError
  const response = await fetch(`https://api.example.com/users/${id}`);
  if (!response.ok) {
    throw new HttpError(response.status, response.url, '取得使用者失敗');
  }
  return response.json();
}

(async () => {
  try {
    const user = await getUser(-1);
    console.log(user);
  } catch (e: unknown) {
    if (e instanceof ValidationError) {
      console.warn('參數驗證錯誤:', e.message);
    } else if (e instanceof HttpError) {
      console.error('API 錯誤:', e.status);
    } else if (e instanceof Error) {
      console.error('一般錯誤:', e.message);
    }
  }
})();

重點:在 async 函式中,所有拋出的錯誤都會被 Promise 包裝,仍然遵循相同的型別檢查流程。


常見陷阱與最佳實踐

陷阱 說明 最佳實踐
拋出非 Error 物件 throw "error"throw 42 會失去堆疊資訊,且在 catch 時需要額外型別判斷。 統一拋出 Error 或其子類別;若必須拋出其他型別,請在 catch 中使用 typeof 或自訂守衛。
直接使用 any 允許任意屬性存取,容易產生執行時錯誤。 開啟 --useUnknownInCatchVariables,或手動將 catch 參數宣告為 unknown
忘記設定自訂錯誤的 prototype 在 ES5 目標下,instanceof 可能失效。 在自訂錯誤建構子裡 Object.setPrototypeOf(this, MyError.prototype)
catch 內部再次拋出未處理的錯誤 會導致錯誤「失蹤」或無法正確上報。 若需要重新拋出,包裝成自訂錯誤,或使用 throw 前先記錄完整資訊。
finally 中吞掉錯誤 finally 內的例外會覆蓋前面的錯誤,難以追蹤。 避免在 finally 內拋出,僅執行清理工作;若必須拋出,先保留原始錯誤資訊。

建議的程式碼風格

  1. 統一錯誤類別:在專案根目錄建立 errors.ts,集中管理 ValidationError, HttpError, DatabaseError 等類別。
  2. 使用 unknown:在 tsconfig.json 中加入 "useUnknownInCatchVariables": true,強制型別守衛。
  3. 型別守衛函式:將常見的錯誤檢查抽離成函式,讓 catch 區塊保持簡潔。
  4. 錯誤日誌:使用 winstonpino 或自訂 logger,確保所有 Error(包含 stack)都有被記錄。
  5. 不在 UI 層直接拋錯:將錯誤轉換成 錯誤代碼 + 使用者友善訊息 再回傳給前端,避免將技術細節直接暴露。

實際應用場景

1. 前端表單驗證

在 React 或 Vue 中,使用 try / catch 包裹表單提交的非同步請求,並根據自訂的 ValidationError 顯示對應的錯誤訊息。

async function onSubmit(data: FormData) {
  try {
    await api.updateProfile(data);
    toast.success('更新成功');
  } catch (e: unknown) {
    if (e instanceof ValidationError) {
      setFieldError(e.field, e.message);
    } else {
      toast.error('系統錯誤,請稍後再試');
    }
  }
}

2. 後端服務的錯誤分類

Node.js + Express 中,使用中介軟體捕捉所有例外,然後根據錯誤類別回傳不同的 HTTP 狀態碼。

app.use((err: unknown, _req, res, _next) => {
  if (err instanceof HttpError) {
    res.status(err.status).json({ error: err.message });
  } else if (err instanceof ValidationError) {
    res.status(400).json({ field: err.field, message: err.message });
  } else {
    console.error(err);
    res.status(500).json({ error: '未知伺服器錯誤' });
  }
});

3. 微服務間的錯誤傳遞

在微服務架構中,服務 A 呼叫服務 B,若 B 回傳錯誤物件,A 需要 保留原始錯誤類別,以便後續決策(重試、降級、告警)。

async function callPaymentService(payload: PaymentDto) {
  try {
    return await httpClient.post('/pay', payload);
  } catch (e: unknown) {
    // 轉換為自訂的 ServiceError,保留原始資訊
    if (e instanceof HttpError) {
      throw new ServiceError('PaymentService', e.status, e.message);
    }
    throw e; // 其他未知錯誤直接傳遞
  }
}

總結

  • try / catch 在 TypeScript 中的錯誤型別預設為 any(或在較新版本中為 unknown),這讓開發者有彈性,但也帶來 型別安全的挑戰
  • 最佳做法
    1. 只拋出 Error 或其子類別,避免字串或其他原始值。
    2. 啟用 unknown,在 catch 中使用 型別守衛instanceof、自訂 guard)來安全存取錯誤屬性。
    3. 自訂錯誤類別(如 ValidationError, HttpError),為錯誤攜帶更多上下文資訊。
    4. 統一錯誤處理:在前端、後端或微服務層面建立共通的錯誤處理機制,讓錯誤訊息、日誌與回傳格式保持一致。

透過上述原則與範例,你可以在 TypeScript 專案中 更精確地捕捉、分類與回報錯誤,提升程式碼的可讀性、可維護性與韌性。祝你在開發過程中少犯錯、少除錯,寫出更健壯的應用程式!