TypeScript 異步與 Promise:Error 型別推論
簡介
在 JavaScript 裡,throw 與 try / catch 是處理非同步錯誤的核心機制。當我們把程式碼搬到 TypeScript 時,除了要考慮執行時的例外外,還必須思考 型別:
catch區塊接收到的值到底是什麼型別?- 我們如何在不失去型別安全的前提下,取得錯誤的詳細資訊?
如果忽視這些問題,最常見的結果就是 any 或 unknown 的濫用,讓編譯器失去保護力,甚至在執行階段產生難以偵測的錯誤。本文將從 Error 型別推論 的角度,說明在 Async / Promise 流程中,如何正確、有效地取得錯誤資訊,並以實務範例展示最佳寫法。
核心概念
1. 為什麼要推論 Error 型別
在 TypeScript 4.0 之後,catch 區塊的變數預設型別從 any 變成 unknown,這是為了強迫開發者自行斷言或檢查。unknown 雖然安全,但如果不加以處理,就會讓錯誤訊息「看不見」:
try {
await fetchData();
} catch (e) {
// e 為 unknown,直接使用 e.message 會編譯錯誤
console.error(e.message); // ❌ TypeScript error
}
因此,我們需要 將 unknown 轉換成更具體的型別(如 Error、自訂錯誤類別),才能安全地存取 message、stack 或自訂屬性。
2. 基本的 Promise 錯誤處理
以下示範最簡單的 async / await + try / catch 寫法,並使用 型別斷言 讓編譯器知道錯誤是 Error:
async function getUser(id: number): Promise<User> {
const resp = await fetch(`/api/users/${id}`);
if (!resp.ok) throw new Error(`User ${id} not found`);
return resp.json();
}
async function showUser(id: number) {
try {
const user = await getUser(id);
console.log('User:', user);
} catch (e) {
// 斷言 e 為 Error
const err = e as Error;
console.error('❗️ 取得使用者失敗:', err.message);
}
}
要點:
- 使用
as Error直接斷言是最直接的方式,但若錯誤來源不一定是Error(例如拋出字串),會失去安全性。
3. unknown vs any:安全的錯誤型別檢查
為了避免過度斷言,我們可以寫一個 type guard,將 unknown 轉為 Error:
function isError(value: unknown): value is Error {
return value instanceof Error;
}
async function updateProfile(data: Profile) {
try {
await api.update(data);
} catch (e) {
if (isError(e)) {
// 此時 e 已被縮小為 Error
console.warn('API 錯誤:', e.message);
} else {
// 不是 Error,可能是字串或自訂物件
console.warn('未知錯誤類型:', e);
}
}
}
重點:
instanceof Error能辨識原生的Error以及繼承自Error的自訂類別。- 當
e不是Error時,我們仍保留unknown,避免隱藏潛在問題。
4. 自訂 Error 類別與型別推論
在大型專案中,常會自訂錯誤類別(例如 HttpError、ValidationError),此時 type guard 需要更精細:
class HttpError extends Error {
constructor(public status: number, message: string) {
super(message);
this.name = 'HttpError';
}
}
function isHttpError(value: unknown): value is HttpError {
return value instanceof HttpError;
}
async function fetchWithAuth(url: string) {
try {
const resp = await fetch(url);
if (!resp.ok) throw new HttpError(resp.status, 'Request failed');
return await resp.json();
} catch (e) {
if (isHttpError(e)) {
// 可以直接取得 status
console.error(`HTTP ${e.status}: ${e.message}`);
} else {
console.error('非 HTTP 錯誤:', e);
}
}
}
這樣的寫法讓 IDE 能正確提示 e.status,同時保留型別安全。
5. Async 函式的返回型別與錯誤推論
async 函式永遠回傳 Promise<T>,即使在 catch 內部重新拋出錯誤,外部仍會得到 Promise<T>,錯誤會被 Reject。若我們想在函式簽名中表達「可能會拋出 HttpError」的資訊,雖然 TypeScript 本身沒有內建的 Promise<…, E>,但可以透過 泛型 搭配 Result 型別自行描述:
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
async function safeFetch(url: string): Promise<Result<any, HttpError>> {
try {
const resp = await fetch(url);
if (!resp.ok) throw new HttpError(resp.status, 'Network error');
const data = await resp.json();
return { ok: true, value: data };
} catch (e) {
if (isHttpError(e)) return { ok: false, error: e };
// 其他錯誤包成 generic Error
return { ok: false, error: e as Error };
}
}
// 使用端
(async () => {
const result = await safeFetch('/api/items');
if (result.ok) {
console.log('資料:', result.value);
} else {
console.error('取得失敗:', result.error.message);
}
})();
透過 Result 型別,我們在 呼叫端 明確知道錯誤的型別,而不必在 catch 中再次做型別判斷。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 直接拋出字串或物件 | throw "Oops"、throw { code: 123 } 會使 catch 變成 unknown,失去 Error 的屬性。 |
只拋出 Error 或其子類別,必要時自行包裝資訊。 |
在 catch 中使用 any |
catch (e: any) 把安全檢查全部關閉,容易誤用。 |
使用預設的 unknown,搭配 type guard 逐層縮小。 |
忘記 await |
async 函式內忘記 await,錯誤會直接變成未處理的 Promise Rejection。 |
確保每個返回 Promise 的呼叫都有 await(或 .catch)。 |
在 finally 中重新拋錯 |
finally 內的錯誤會 覆寫 先前的錯誤,導致原始錯誤資訊遺失。 |
只在 finally 做清理工作,若需要拋錯,先保存原始錯誤。 |
| 忽略自訂錯誤的屬性 | 自訂錯誤類別有額外屬性(如 status),但在 catch 中只使用 e.message。 |
使用對應的 type guard,取得全部自訂屬性。 |
最佳實踐:
- 統一錯誤類別:在專案根目錄建立
errors.ts,集中管理所有自訂錯誤。 - 使用 type guard:每個自訂錯誤都提供
isXError函式,讓catch內部保持乾淨。 - 避免
any:除非真的無法避免,否則堅持使用unknown+ 類型縮小。 - 錯誤資訊串接:在拋錯前加入上下文(如 API 路徑、參數),方便除錯。
實際應用場景
1. 前端 UI 錯誤訊息顯示
在 React + TypeScript 專案中,常見的需求是「根據錯誤類別顯示不同的 toast」:
import { isHttpError } from './errors';
async function submitForm(data: FormData) {
try {
await api.save(data);
toast.success('儲存成功');
} catch (e) {
if (isHttpError(e) && e.status === 401) {
toast.error('未授權,請重新登入');
} else if (e instanceof Error) {
toast.error(e.message);
} else {
toast.error('未知錯誤,請稍後再試');
}
}
}
透過 isHttpError,UI 能直接根據 HTTP 狀態碼 做客製化回饋。
2. Node.js 後端服務的統一錯誤處理
在 Express + TypeScript 中,我們可以把所有路由的錯誤轉成 Result,最後交給全域錯誤中介軟體:
// middleware/resultHandler.ts
import { Request, Response, NextFunction } from 'express';
import { Result } from './result';
export function resultHandler(
req: Request,
res: Response,
next: NextFunction
) {
// 假設所有路由都回傳 Promise<Result>
(req as any).result
.then((result: Result<any, Error>) => {
if (result.ok) {
res.json(result.value);
} else {
// 統一回傳錯誤格式
res.status(500).json({ error: result.error.message });
}
})
.catch(next); // 未捕獲的錯誤交給 Express default handler
}
路由:
router.get('/items', async (req, res, next) => {
(req as any).result = safeFetch('/api/items');
});
這樣的設計把 錯誤型別推論 前置於服務層,讓最終的錯誤回應保持一致。
總結
- Error 型別推論 是保護 TypeScript 程式碼在非同步環境中不被隱藏例外的關鍵。
- 從
unknown開始,使用 type guard 把錯誤縮小為Error、HttpError或其他自訂類別,才能安全存取message、stack、自訂屬性。 - 自訂錯誤類別 搭配
instanceof判斷,可讓 IDE 提供完整的屬性提示,提升開發效率。 - 若想在函式簽名中表達錯誤型別,
Result<T, E>之類的泛型封裝是一個實用的方案,讓呼叫端在編譯期即得知可能的錯誤。 - 避免常見陷阱(拋出非 Error、使用
any、忽略自訂屬性),並遵循 統一錯誤類別、type guard、錯誤資訊串接 的最佳實踐。
掌握了這套 Error 型別推論 的思維後,你的 TypeScript 非同步程式碼將更加可預測、易除錯、且具備良好的型別安全,在前端 UI、Node.js 後端甚至跨平台的全端專案中,都能發揮巨大的價值。祝你寫程式開心,錯誤處理更順手!