JavaScript 教學:錯誤與除錯 ── 自訂錯誤類型(Custom Error)
簡介
在日常開發中,我們往往會遇到各式各樣的錯誤:語法錯誤、執行時例外、業務邏輯不符合規則…如果只能依賴原生的 Error 物件,往往難以在程式碼中快速定位問題根源,也不利於向呼叫端傳遞清楚、結構化的錯誤資訊。
自訂錯誤類型(Custom Error)正是為了解決這個痛點而生。透過繼承 Error,我們可以為不同的錯誤情境建立專屬的類別,讓錯誤訊息更具語意、堆疊資訊更清楚,並且在捕獲(catch)時能以 instanceof 判斷錯誤類別,實作更精細的錯誤處理流程。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,一直到實務應用場景,帶你一步步掌握 自訂錯誤類型 的寫法與使用方式,適合剛接觸 JavaScript 的新手,也能為中級開發者提供更完整的除錯技巧。
核心概念
1. 為什麼要自訂錯誤?
原生 Error |
自訂錯誤 |
|---|---|
| 訊息只能是單一字串 | 可以加入自訂屬性(如 code, status, payload) |
| 無法直接辨識錯誤來源 | 透過類別名稱即可判斷錯誤類型 |
| 捕獲時只能靠字串比對 | 可使用 instanceof 或 error.constructor.name |
重點:自訂錯誤讓錯誤資訊更具結構,也讓錯誤處理程式碼更具可讀性與可維護性。
2. 繼承 Error 的基本寫法
在 ES6 以前,我們需要手動設定 prototype、name、stack 等屬性;而在 ES6+(包括 TypeScript)中,只要 extends Error,大部分工作交給 JavaScript 引擎處理即可。
class MyError extends Error {
constructor(message) {
super(message); // 呼叫 Error 的建構子,設定訊息與 stack
this.name = this.constructor.name; // 設定錯誤名稱為類別名稱
// 若要在瀏覽器或 Node.js 中正確捕獲 stack,可額外寫以下兩行
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
小技巧:
this.name = this.constructor.name讓錯誤名稱自動與類別保持同步,避免手動寫錯。
3. 為錯誤加入自訂屬性
自訂錯誤最常見的需求是把錯誤代碼(error code)或 HTTP 狀態碼(status code)一起帶出,讓呼叫端能依據代碼做不同的回應。
class ValidationError extends Error {
/**
* @param {string} message 錯誤訊息
* @param {string[]} fields 失敗驗證的欄位名稱
*/
constructor(message, fields) {
super(message);
this.name = this.constructor.name;
this.fields = fields; // 失敗的欄位清單
this.code = 'VALIDATION_ERR'; // 自訂錯誤代碼
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
4. 使用 instanceof 判斷錯誤類型
try {
// 假設下面的函式會拋出 ValidationError
validateUserInput(data);
} catch (err) {
if (err instanceof ValidationError) {
console.warn('驗證失敗:', err.fields);
// 針對驗證錯誤回傳 400 Bad Request
} else if (err instanceof MyError) {
console.error('自訂錯誤:', err.message);
} else {
console.error('未知錯誤:', err);
}
}
提醒:
instanceof只能在同一執行環境(同一個realm)下正確判斷;跨窗口或跨 Node.js worker 時,建議使用err.name === 'ValidationError'。
5. 多層級的錯誤類別結構
對大型專案而言,將錯誤分類成「系統錯誤 → 資料庫錯誤 → 業務錯誤」等層級,可讓錯誤處理更有彈性。
class AppError extends Error {
constructor(message, options = {}) {
super(message);
this.name = this.constructor.name;
this.code = options.code || 'APP_ERR';
this.status = options.status || 500; // HTTP 狀態碼
this.payload = options.payload; // 任意附帶資料
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
class DatabaseError extends AppError {
constructor(message, query) {
super(message, { code: 'DB_ERR', status: 500 });
this.query = query; // 失敗的 SQL / NoSQL 查詢
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, { code: 'NOT_FOUND', status: 404 });
this.resource = resource;
}
}
程式碼範例
下面提供 5 個實用範例,從最簡單的自訂錯誤到結合 HTTP API 的完整流程,說明每一步的意圖與注意事項。
範例 1:最簡單的自訂錯誤
class SimpleError extends Error {
constructor(message) {
super(message);
this.name = 'SimpleError';
}
}
// 使用
function doSomething(flag) {
if (!flag) throw new SimpleError('flag 必須為 true');
}
try {
doSomething(false);
} catch (e) {
console.log(e.name); // SimpleError
console.log(e.message); // flag 必須為 true
}
說明:只改寫
name,即可在捕獲時快速辨識錯誤類型。
範例 2:帶有錯誤代碼的驗證錯誤
class ValidationError extends Error {
constructor(message, fields) {
super(message);
this.name = 'ValidationError';
this.fields = fields; // 失敗的欄位
this.code = 'VALIDATION_ERROR';
}
}
// 檢查使用者輸入
function validate(data) {
const errors = [];
if (!data.email.includes('@')) errors.push('email');
if (data.age < 0) errors.push('age');
if (errors.length) {
throw new ValidationError('資料驗證失敗', errors);
}
}
// 捕獲示範
try {
validate({ email: 'wrongemail', age: -5 });
} catch (err) {
if (err instanceof ValidationError) {
console.warn(`錯誤代碼:${err.code}`, err.fields);
// => 錯誤代碼:VALIDATION_ERROR [ 'email', 'age' ]
}
}
範例 3:結合 HTTP 狀態碼的應用錯誤
class HttpError extends Error {
/**
* @param {number} status HTTP 狀態碼
* @param {string} message 錯誤訊息
*/
constructor(status, message) {
super(message);
this.name = 'HttpError';
this.status = status;
this.code = `HTTP_${status}`;
}
}
// Express 範例
const express = require('express');
const app = express();
app.get('/resource/:id', (req, res, next) => {
const resource = database.find(req.params.id);
if (!resource) {
// 拋出自訂 404 錯誤
return next(new HttpError(404, `資源 ${req.params.id} 不存在`));
}
res.json(resource);
});
// 統一錯誤處理中介軟體
app.use((err, req, res, next) => {
if (err instanceof HttpError) {
res.status(err.status).json({ code: err.code, message: err.message });
} else {
console.error(err);
res.status(500).json({ code: 'UNKNOWN', message: '伺服器錯誤' });
}
});
關鍵:在 Express 中把錯誤
next(err),讓統一的錯誤處理中介軟體負責回應,程式碼維護性大幅提升。
範例 4:多層級錯誤結構(AppError → DatabaseError)
class AppError extends Error {
constructor(message, { code = 'APP_ERROR', status = 500, payload } = {}) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.status = status;
this.payload = payload;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
class DatabaseError extends AppError {
constructor(message, query) {
super(message, { code: 'DB_ERROR', status: 500 });
this.query = query; // 失敗的查詢語句
}
}
// 模擬 DB 呼叫
function queryDatabase(sql) {
// 假設查詢失敗
throw new DatabaseError('資料庫執行錯誤', sql);
}
try {
queryDatabase('SELECT * FROM users');
} catch (err) {
if (err instanceof DatabaseError) {
console.error(`代碼: ${err.code}, 查詢: ${err.query}`);
// => 代碼: DB_ERROR, 查詢: SELECT * FROM users
}
}
範例 5:在 Promise/async 中拋出自訂錯誤
class TimeoutError extends Error {
constructor(message = '操作逾時') {
super(message);
this.name = 'TimeoutError';
this.code = 'TIMEOUT';
}
}
/**
* 模擬一個可能超時的非同步任務
* @param {number} ms 任務執行時間(毫秒)
*/
function asyncTask(ms) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
resolve('完成');
}, ms);
// 5 秒內未完成則拋出 TimeoutError
setTimeout(() => {
clearTimeout(timer);
reject(new TimeoutError(`任務執行超過 ${ms}ms`));
}, 5000);
});
}
// 使用 async/await
(async () => {
try {
const result = await asyncTask(8000); // 故意讓它超時
console.log(result);
} catch (err) {
if (err instanceof TimeoutError) {
console.warn(`捕獲到超時錯誤:${err.message}`);
} else {
console.error(err);
}
}
})();
實務觀點:在非同步流程中,
instanceof同樣適用,只要保證錯誤類別在同一個執行環境中被引用。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記呼叫 super(message) |
不呼叫父類別建構子會導致 stack 為 undefined,且錯誤訊息不會正確傳遞。 |
必須在子類別建構子第一行 super(message)。 |
this.name 沒設正確 |
Error 的 name 預設是 "Error",若不改會讓日誌看不出錯誤類型。 |
設定 this.name = this.constructor.name,或手動指定字串。 |
跨執行環境 instanceof 失效 |
在 iframe、Web Worker、Node worker_threads 中,instanceof 無法正確判斷。 |
改用 err.name === 'MyError' 或檢查自訂屬性 (err.code). |
| 堆疊資訊缺失 | 部分舊版瀏覽器不支援 Error.captureStackTrace,stack 可能不完整。 |
在支援的環境使用 Error.captureStackTrace(this, this.constructor);不支援時可自行組裝 stack。 |
| 拋出原始字串或物件 | 只拋出字串或普通物件會失去錯誤類別與堆疊資訊。 | 永遠拋出 Error 或其子類別的實例。 |
最佳實踐
統一基礎錯誤類別
為專案建立一個AppError(或BaseError)作為所有自訂錯誤的根類別,方便未來在全局捕獲時一次處理。錯誤代碼(code)與 HTTP 狀態碼(status)分離
code用於業務層面的錯誤識別(如VALIDATION_ERR、DB_CONN_FAIL)。status僅在與 HTTP 互動時才使用,保持錯誤本身的通用性。
錯誤訊息與使用者訊息分離
message給開發者除錯用。- 若要回傳給前端,請在錯誤類別中加入
userMessage或在捕獲層自行翻譯。
在中間層(Middleware)統一處理
在 Express、Koa、NestJS 等框架中,建立一個全域錯誤處理器,根據err instanceof或err.code統一回應。記錄(logging)時保留完整
stack
使用winston、pino等日誌套件,將err.stack一併寫入,方便事後追蹤。
實際應用場景
1. 表單驗證(前端 & 後端)
- 前端:在 Vue/React 表單提交前,若驗證失敗拋出
ValidationError,捕獲後直接在 UI 顯示欄位錯誤。 - 後端:API 收到不合法的 JSON 時,拋出相同的
ValidationError,統一返回400 Bad Request,前端僅需根據錯誤代碼映射 UI。
2. 第三方 API 呼叫失敗
class ExternalServiceError extends AppError {
constructor(serviceName, status, details) {
super(`${serviceName} 回傳錯誤`, {
code: `${serviceName.toUpperCase()}_ERR`,
status,
payload: details,
});
this.serviceName = serviceName;
}
}
- 呼叫 Stripe、AWS、Firebase 等外部服務時,若回傳非 2xx,拋出
ExternalServiceError,在全局捕獲後決定是否重試或回報給使用者。
3. 資料庫操作錯誤
- 依據錯誤類型(連線失敗、語法錯誤、唯一鍵衝突)拋出
DatabaseError、DuplicateKeyError等子類別,讓服務層可根據錯誤類別返回不同的 HTTP 狀態(如409 Conflict)。
4. 工作排程(Cron / Queue)
- 在背景工作(Node.js worker、RabbitMQ consumer)中,若任務因資源不足而失敗,拋出
RetryableError,在外層捕獲後將任務重新放回佇列。
5. API 金鑰或權限驗證
class AuthError extends AppError {
constructor(message = '未授權') {
super(message, { code: 'AUTH_ERR', status: 401 });
this.name = 'AuthError';
}
}
- 在中介層檢查 JWT、API Key 時,若驗證失敗直接拋出
AuthError,統一回傳401 Unauthorized。
總結
自訂錯誤類型不僅是 程式碼可讀性 的提升,更是 除錯與錯誤治理 的關鍵工具。透過繼承 Error、加入錯誤代碼、狀態碼與自訂屬性,我們可以:
- 快速定位問題:
instanceof讓錯誤類型一目了然。 - 提供結構化資訊:前端、日誌、監控系統皆可直接使用
code、status、payload。 - 統一錯誤處理流程:在中間層或全局捕獲器中一次處理所有自訂錯誤,減少重複程式碼。
- 提升使用者體驗:將技術錯誤與使用者可讀訊息分離,避免將堆疊資訊直接暴露給前端。
在日常開發中,建議先建立一個 基礎錯誤類別(如 AppError),再根據業務需求逐步擴充子類別;同時配合適當的日誌與監控,讓錯誤不再是「未知」的噩夢,而是可控、可追蹤的資訊。
祝你在 JavaScript 的錯誤與除錯旅程中,能夠寫出更乾淨、更穩定的程式碼! 🚀