JavaScript 錯誤與除錯:深入瞭解 Error 物件 的 name、message 與 stack
簡介
在 JavaScript 開發過程中,錯誤(Error) 是不可避免的。無論是語法錯誤、執行時例外,或是自訂業務例外,正確地取得錯誤資訊是定位問題、提升程式品質的關鍵。Error 物件本身提供了三個最常被使用的屬性:name、message 與 stack。掌握它們的意義與用法,能讓你在 除錯(debugging) 時快速定位問題根源,亦能在 錯誤處理(error handling) 時提供使用者友善的回饋。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步熟悉這三個屬性,並示範它們在真實專案中的應用方式。文章以繁體中文(台灣)撰寫,適合 初學者到中級開發者 閱讀,並配合大量程式碼範例,讓你立即上手。
核心概念
1. Error 物件的基本結構
在 JavaScript 中,所有錯誤皆繼承自全域的 Error 建構子。最簡單的建立方式如下:
const err = new Error('發生未知錯誤');
console.log(err);
執行結果會顯示一個 Error 物件,包含以下預設屬性:
| 屬性 | 型別 | 說明 |
|---|---|---|
name |
string |
錯誤類型的名稱,預設為 "Error" |
message |
string |
錯誤的描述訊息,由建構子參數傳入 |
stack |
string |
堆疊追蹤(stack trace),顯示錯誤發生時的呼叫路徑(瀏覽器或 Node.js 會自動產生) |
註:
stack屬性不是 ECMAScript 標準的一部份,但在主流環境(Chrome、Firefox、Node)皆支援,且在除錯時最有價值。
2. name:辨識錯誤類型
name 用來說明錯誤的類別,預設為 "Error",但你可以透過子類別或自行修改來區分不同情境:
// 內建子類別
try {
JSON.parse('不是 JSON');
} catch (e) {
console.log(e.name); // SyntaxError
}
// 自訂錯誤類別
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError'; // 明確指定類型名稱
}
}
實務意義
- 在
catch區塊裡可根據name做條件分支,提供不同的錯誤處理策略。 - 日誌系統(logging)常利用
name來篩選與統計錯誤類型。
3. message:錯誤的說明文字
message 是開發者自行提供的描述,應該 具體、易讀,讓維護者或使用者能快速了解問題。
function divide(a, b) {
if (b === 0) {
throw new Error('除數不可為 0');
}
return a / b;
}
最佳實踐
- 避免使用過於簡短的訊息(如
"錯誤"),也不要直接暴露內部實作細節(如資料庫連線字串)。 - 若錯誤與參數相關,可把參數值寫入
message,提升可追溯性。
throw new Error(`除數不可為 0,實際收到的值為 ${b}`);
4. stack:堆疊追蹤的力量
stack 會列出錯誤拋出時的呼叫路徑,通常包含檔案名稱、行號與函式名稱。它是 除錯的金礦,尤其在大型應用程式中,能快速定位是哪一段程式碼拋出了例外。
function foo() {
bar();
}
function bar() {
baz();
}
function baz() {
throw new Error('故意拋出錯誤');
}
try {
foo();
} catch (e) {
console.log(e.stack);
}
執行結果(Chrome):
Error: 故意拋出錯誤
at baz (example.js:9:9)
at bar (example.js:5:3)
at foo (example.js:1:3)
at <anonymous> (example.js:13:3)
實務應用
- 日誌系統:將
stack寫入檔案或遠端服務,以便事後分析。 - 錯誤上報:在前端應用中,把
stack送到後端的錯誤監控平台(如 Sentry、BugSnag)。 - 自訂錯誤類別:在子類別中保留或重新格式化
stack,讓開發者看到更清晰的呼叫層級。
程式碼範例
以下提供 5 個實用範例,示範如何在不同情境下使用 name、message 與 stack。
範例 1:基本的 try…catch 與屬性讀取
try {
// 故意觸發 ReferenceError
nonExistentVariable;
} catch (err) {
console.log('錯誤名稱 :', err.name); // ReferenceError
console.log('錯誤訊息 :', err.message); // nonExistentVariable is not defined
console.log('堆疊資訊 :\n', err.stack); // 包含檔案與行號
}
重點:即使是原生錯誤,
name、message、stack仍可直接取得。
範例 2:自訂錯誤類別並加入額外資訊
class ApiError extends Error {
constructor(status, url, message) {
super(message);
this.name = 'ApiError';
this.status = status; // HTTP 狀態碼
this.url = url; // 請求的 URL
// 讓 stack 更有意義(只保留自訂層級)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ApiError);
}
}
}
// 使用範例
function fetchData(endpoint) {
// 假設收到 404 錯誤
throw new ApiError(404, endpoint, '找不到資源');
}
try {
fetchData('/api/users/123');
} catch (e) {
console.error(`[${e.name}] ${e.message} (status: ${e.status})`);
console.error('發生於 URL:', e.url);
console.error('堆疊資訊:\n', e.stack);
}
說明:自訂錯誤類別可以保留額外屬性(如 status、url),同時仍保有 name、message、stack 的完整資訊,方便後續的錯誤上報與分析。
範例 3:在 Node.js 中捕獲非同步錯誤
const fs = require('fs').promises;
async function readConfig() {
try {
const data = await fs.readFile('./config.json', 'utf8');
return JSON.parse(data);
} catch (err) {
// 可能是檔案不存在、JSON 解析錯誤或其他 IO 錯誤
console.error('錯誤類型:', err.name);
console.error('錯誤訊息:', err.message);
console.error('堆疊追蹤:\n', err.stack);
// 重新拋出自訂錯誤,讓上層決定如何處理
throw new Error('讀取設定檔失敗');
}
}
readConfig().catch(e => console.error('最外層捕獲:', e.message));
關鍵:即使在 async/await 流程中,catch 仍能取得完整的 stack,有助於定位非同步錯誤的根本來源。
範例 4:利用 stack 產生友善的錯誤訊息
function formatError(err) {
// 只保留前兩層呼叫資訊,避免過長
const stackLines = err.stack.split('\n').slice(0, 3);
return `${err.name}: ${err.message}\n於 ${stackLines.join('\n')}`;
}
function a() { b(); }
function b() { c(); }
function c() { throw new Error('發生意外'); }
try {
a();
} catch (e) {
console.log(formatError(e));
}
輸出:
Error: 發生意外
於 Error: 發生意外
at c (<anonymous>:4:9)
at b (<anonymous>:3:3)
實務應用:在前端 UI 中顯示簡短的錯誤資訊給使用者,同時在後端保留完整 stack 供開發者除錯。
範例 5:全局錯誤監聽與自動上報
// 前端全局錯誤處理(window.onerror)
window.onerror = function (msg, src, line, col, error) {
const payload = {
name: error?.name || 'Error',
message: msg,
stack: error?.stack || `at ${src}:${line}:${col}`
};
// 假設有一個上報 API
fetch('/api/reportError', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
// 防止錯誤再次冒泡
return true;
};
說明:透過全局監聽,我們可以在任何未被捕獲的錯誤發生時,取得 name、message、stack,並自動上傳至後端做集中監控。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 最佳做法 |
|---|---|---|
未設定 name |
自訂錯誤類別忘記改寫 this.name,會被視為一般 Error。 |
在子類別建構子裡明確指定 this.name。 |
stack 失真 |
某些瀏覽器(如 IE)不支援 stack,或在壓縮後的程式碼中行號不正確。 |
使用 source map(.map)讓壓縮檔的 stack 仍能對應到原始檔。 |
| 過度暴露資訊 | 把機密資訊(如資料庫帳號)寫入 message 或 stack,會在前端或日誌中洩漏。 |
僅在開發環境保留完整堆疊,正式環境只上傳錯誤代碼或簡短訊息。 |
| 忽略非同步錯誤 | try…catch 只能捕捉同步錯誤,Promise、async/await 需要額外處理。 |
為每個 Promise 加上 .catch(),或在 async 函式內使用 try…catch。 |
| 直接拋出字串 | throw 'error' 只會得到字串,缺少 name、stack,不易除錯。 |
永遠拋出 Error 或其子類別的實例。 |
總結最佳實踐
- 統一使用
Error物件:自訂錯誤時繼承Error,確保stack正確生成。 - 明確命名
name:讓錯誤類型一目了然,方便條件分支與日誌分析。 - 提供具體且安全的
message:描述問題所在,避免洩漏敏感資訊。 - 保存完整
stack:在開發環境或除錯時完整記錄,正式環境可視需求截斷。 - 結合 source map:讓壓縮後的程式碼仍能回溯到原始行號。
實際應用場景
1. 前端表單驗證
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new ValidationError(`電子郵件格式不正確:${email}`);
}
}
在 catch 中根據 e.name === 'ValidationError' 直接顯示友善訊息給使用者,而把 e.stack 上報給後端進行統計。
2. 伺服器端 API 錯誤統計
在 Node.js 中,使用中間件捕獲所有未處理的例外,記錄 name、message、stack,並依 name 分類存入資料庫,方便日後產生錯誤報表。
app.use((err, req, res, next) => {
logger.error({
name: err.name,
message: err.message,
stack: err.stack,
path: req.path,
method: req.method
});
res.status(500).json({ error: '系統發生錯誤,請稍後再試' });
});
3. 客戶端自動上報(Sentry)
Sentry.init({ dsn: 'https://example@sentry.io/12345' });
window.onerror = (msg, src, line, col, error) => {
Sentry.captureException(error || new Error(msg));
};
Sentry 會自動擷取 name、message、stack,並顯示在儀表板上,協助團隊快速定位問題。
總結
Error 物件的 name、message、stack 三大屬性是 JavaScript 錯誤處理與除錯的核心資訊。
name幫助我們辨識錯誤類型,適合在catch中做條件分支。message應提供具體且安全的描述,讓使用者與開發者都能快速了解問題。stack則是除錯的金礦,配合 source map 能在壓縮程式碼中仍保有可讀的行號資訊。
透過 自訂錯誤類別、全局錯誤監聽、日誌與上報機制,我們能將錯誤資訊系統化、可追溯,進而提升程式的穩定性與使用者體驗。
掌握這些概念後,你將能在開發過程中更從容地面對各種例外,並在專案規模擴大時仍保持良好的錯誤管理與除錯效率。祝你寫程式順利,錯誤處理無懈可擊! 🚀