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 編譯選項開啟)。unknown 與 any 的差別在於:
| 特性 | any |
unknown |
|---|---|---|
| 直接存取屬性 | ✅(不會報錯) | ❌(需要型別斷言或守衛) |
| 直接呼叫方法 | ✅ | ❌ |
| 可賦值給其他類型 | ✅(任意) | ❌(只能賦值給 any、unknown) |
使用 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))內部自動推斷e為HttpError。
4.3. typeof 與 Array.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內建name、message、stack,適合大多數錯誤情境,且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);
}
}
重點:自訂錯誤可攜帶額外資訊(如
status、url),在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️⃣:結合 Promise 與 async/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 內拋出,僅執行清理工作;若必須拋出,先保留原始錯誤資訊。 |
建議的程式碼風格
- 統一錯誤類別:在專案根目錄建立
errors.ts,集中管理ValidationError,HttpError,DatabaseError等類別。 - 使用
unknown:在tsconfig.json中加入"useUnknownInCatchVariables": true,強制型別守衛。 - 型別守衛函式:將常見的錯誤檢查抽離成函式,讓
catch區塊保持簡潔。 - 錯誤日誌:使用
winston、pino或自訂 logger,確保所有Error(包含 stack)都有被記錄。 - 不在 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),這讓開發者有彈性,但也帶來 型別安全的挑戰。- 最佳做法:
- 只拋出
Error或其子類別,避免字串或其他原始值。 - 啟用
unknown,在catch中使用 型別守衛(instanceof、自訂 guard)來安全存取錯誤屬性。 - 自訂錯誤類別(如
ValidationError,HttpError),為錯誤攜帶更多上下文資訊。 - 統一錯誤處理:在前端、後端或微服務層面建立共通的錯誤處理機制,讓錯誤訊息、日誌與回傳格式保持一致。
- 只拋出
透過上述原則與範例,你可以在 TypeScript 專案中 更精確地捕捉、分類與回報錯誤,提升程式碼的可讀性、可維護性與韌性。祝你在開發過程中少犯錯、少除錯,寫出更健壯的應用程式!