前端JS異常捕獲與統(tǒng)一格式化的完整指南
引言
在前端開發(fā)中,異常監(jiān)控是保證應(yīng)用穩(wěn)定性的重要一環(huán)。當(dāng)用戶遇到頁面白屏、功能不可用等問題時,如果能及時收集到詳細(xì)的錯誤信息(包括堆棧、行列號、瀏覽器環(huán)境等),就能快速定位并修復(fù) bug。瀏覽器提供了 window.onerror 和 unhandledrejection 兩個全局事件,分別用于捕獲未處理的 JavaScript 異常和未捕獲的 Promise 拒絕。然而,不同瀏覽器對這些事件的參數(shù)支持存在差異,錯誤對象的格式也各不相同,如何編寫一個兼容所有瀏覽器、并能像 console.log(error) 那樣輸出完整堆棧的格式化函數(shù),是搭建前端監(jiān)控系統(tǒng)的第一步。
本文將帶你深入理解 console.log(error) 的底層實現(xiàn),并給出一個通用的錯誤格式化方案,最后演示如何將格式化后的異常信息上報到后端。
為什么需要統(tǒng)一格式化
當(dāng)你在控制臺直接執(zhí)行 console.log(new Error('something wrong')) 時,瀏覽器會打印出類似這樣的信息:
Error: something wrong
at <anonymous>:1:13
at ...
但如果使用 window.onerror 捕獲,你拿到的參數(shù)可能只有消息、腳本 URL、行號、列號和一個可選的 error 對象。這些參數(shù)組合起來未必能還原出完整的堆棧。此外,unhandledrejection 的 reason 可能是任意類型(字符串、對象、Error 實例等),如何安全地提取信息并拼接成可讀的字符串,也需要仔細(xì)處理。
一個優(yōu)秀的異常上報方案應(yīng)該做到:
- 完整性:盡可能包含錯誤名稱、消息、調(diào)用堆棧、發(fā)生位置(文件、行號、列號)。
- 兼容性:支持所有主流瀏覽器(包括 IE9+)。
- 健壯性:處理循環(huán)引用、非 Error 對象等特殊情況,避免二次異常。
- 一致性:最終上報的字符串格式統(tǒng)一,便于后端解析或搜索。
console.log(error)的底層原理
在深入實現(xiàn)之前,我們先了解一下瀏覽器是如何打印錯誤對象的。以 Chrome 的 V8 引擎為例:
console.log接收一個對象后,會調(diào)用該對象的[Symbol.toStringTag]或自定義的inspect方法(DevTools 擴展)。對于 Error 對象,V8 內(nèi)部會檢查其是否有stack屬性。error.stack是一個非標(biāo)準(zhǔn)但所有現(xiàn)代瀏覽器都支持的屬性,它包含了當(dāng)前調(diào)用棧的快照。這個堆棧字符串的生成依賴于Error.captureStackTrace(Node.js 中)或運行時自動收集的調(diào)用幀。- 如果
error.stack存在,瀏覽器直接輸出該字符串;否則,退而使用error.toString()(通常是"Error: message"的形式)。
因此,要獲得與 console.log 相同的輸出,我們只需在全局事件中盡量獲取到 error.stack 即可。當(dāng)無法獲取 stack 時,再根據(jù)事件參數(shù)手動拼接位置信息。
統(tǒng)一錯誤格式化函數(shù)
下面是一個健壯的 formatError 函數(shù),它接受任意類型的錯誤值以及可選的 URL、行號、列號,返回格式化的錯誤字符串。
/**
* 將任意錯誤值格式化為包含堆棧信息的字符串
* @param {*} error - 錯誤對象或任意值
* @param {string} fallbackMessage - 當(dāng)無法獲取有效信息時的備選消息
* @param {string} [url] - 發(fā)生錯誤的腳本URL(從onerror獲取)
* @param {number} [line] - 行號(從onerror獲?。?
* @param {number} [col] - 列號(從onerror獲取)
* @returns {string} 格式化后的錯誤字符串
*/
function formatError(error, fallbackMessage, url, line, col) {
let result = '';
// 情況1:error 是對象類型,嘗試提取 stack 或 message
if (error && typeof error === 'object') {
// 優(yōu)先使用 stack(包含完整的調(diào)用堆棧)
if (typeof error.stack === 'string') {
result = error.stack;
}
// 其次使用標(biāo)準(zhǔn) error 屬性(name 和 message)
else if (typeof error.message === 'string') {
const name = error.name || 'Error';
result = `${name}: ${error.message}`;
}
// 否則嘗試 JSON 序列化(避免循環(huán)引用)
else {
try {
result = JSON.stringify(error, null, 2);
} catch (e) {
// 序列化失?。ㄈ缪h(huán)引用),使用默認(rèn)字符串轉(zhuǎn)換
result = String(error);
}
}
} else {
// 原始類型直接轉(zhuǎn)為字符串
result = String(error);
}
// 情況2:結(jié)果中不包含行列信息(如只拿到 message),但通過 onerror 獲得了具體位置
// 簡單判斷堆棧中是否已有類似 ":數(shù)字" 的行號標(biāo)記
const hasLineInfo = /:\d+/.test(result);
if (!hasLineInfo && url && line) {
const location = `${url}:${line}${col ? ':' + col : ''}`;
result = result ? `${result} at ${location}` : `Error at ${location}`;
}
// 情況3:仍然沒有有效內(nèi)容,使用 fallbackMessage
if (!result && fallbackMessage) {
result = fallbackMessage;
}
return result;
}
關(guān)鍵點說明
- 優(yōu)先使用
error.stack:只要錯誤對象有 stack 屬性,就直接使用它,因為 stack 已經(jīng)包含了最完整的調(diào)用鏈和位置信息。 - 降級使用
name和message:如果對象是 Error 實例但 stack 可能被篡改或不存在,則拼接name: message。 - JSON 序列化兜底:對于普通對象(如
{ code: 500, msg: 'fail' }),嘗試用 JSON.stringify 展示其結(jié)構(gòu),并捕獲循環(huán)引用異常。 - 附加行列號:當(dāng)最終字符串中沒有明顯的數(shù)字位置(如
:10)且外部提供了 URL 和行號時,將位置信息附加到末尾。這可以彌補某些場景下error.stack缺失行列的不足。 - fallbackMessage 參數(shù):當(dāng) error 為
undefined或空值時,可以傳入默認(rèn)消息,例如'Unhandled Rejection'。
全局監(jiān)聽器:window.onerror 和 unhandledrejection
有了格式化函數(shù),我們就可以在全局事件中調(diào)用它,并將結(jié)果上報。
window.onerror
window.onerror = function (message, source, lineno, colno, error) {
const errorStr = formatError(error, message, source, lineno, colno);
// 上報錯誤(示例:使用 sendToServer 函數(shù))
sendToServer({
type: 'onerror',
message: message,
stack: errorStr,
url: source,
line: lineno,
column: colno,
userAgent: navigator.userAgent,
timestamp: Date.now()
});
// 返回 true 可以阻止瀏覽器默認(rèn)處理(如控制臺打印錯誤)
// return true;
};
注意:舊版 IE(<=10)不會傳遞 error 參數(shù),此時 error 為 undefined,我們的 formatError 會使用 fallbackMessage(即 message)和行列號來構(gòu)造字符串。
unhandledrejection
window.addEventListener('unhandledrejection', function (event) {
const reason = event.reason;
const errorStr = formatError(reason, 'Unhandled Rejection');
sendToServer({
type: 'unhandledrejection',
reason: errorStr,
userAgent: navigator.userAgent,
timestamp: Date.now()
});
// 可選:阻止默認(rèn)行為(某些瀏覽器會打印錯誤)
event.preventDefault();
});
event.reason 可以是任何類型,我們的 formatError 已經(jīng)做了充分處理。
上報函數(shù)實現(xiàn)
最簡單的上報可以通過 navigator.sendBeacon 或 fetch 發(fā)送到后端接口。為了不影響用戶體驗,建議使用 sendBeacon,它會在頁面卸載時也能確保請求發(fā)出。
function sendToServer(data) {
// 避免頻繁上報(例如使用采樣率)
if (Math.random() > 0.1) return; // 10% 采樣
const url = 'https://your-monitor-server.com/api/error';
const body = JSON.stringify(data);
if (navigator.sendBeacon) {
navigator.sendBeacon(url, body);
} else {
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body,
keepalive: true // 類似 sendBeacon 的行為
}).catch(() => {}); // 忽略 fetch 失敗
}
}
兼容性深度解析
不同瀏覽器的window.onerror參數(shù)
| 瀏覽器 | message | source | lineno | colno | error |
|---|---|---|---|---|---|
| Chrome / Firefox / Safari / Edge (現(xiàn)代) | ?? | ?? | ?? | ?? | ?? |
| IE 10+ | ?? | ?? | ?? | ?? | ??(但可能為 null) |
| IE 9- | ?? | ?? | ?? | ? | ? |
我們的 formatError 能夠適應(yīng)以上所有情況:當(dāng) error 不存在時,利用 message、source、lineno 構(gòu)造一個簡化版本。
堆棧格式差異
不同瀏覽器生成的 error.stack 格式略有不同,例如:
- Chrome:
Error: message\n at function (file:line:column) - Firefox:
Error: message\n function@file:line:column - Safari:
Error: message\n function@file:line:column - IE:
Error: message\n at function (file:line:column)
這些格式差異通常不影響可讀性,我們的格式化函數(shù)直接保留原始 stack,不進行解析和重組,以保證信息不丟失。
完整示例代碼
將上述片段整合,得到一個完整的監(jiān)控模塊:
// error-monitor.js
(function() {
'use strict';
function formatError(error, fallbackMessage, url, line, col) {
let result = '';
if (error && typeof error === 'object') {
if (typeof error.stack === 'string') {
result = error.stack;
} else if (typeof error.message === 'string') {
const name = error.name || 'Error';
result = `${name}: ${error.message}`;
} else {
try {
result = JSON.stringify(error, null, 2);
} catch (e) {
result = String(error);
}
}
} else {
result = String(error);
}
const hasLineInfo = /:\d+/.test(result);
if (!hasLineInfo && url && line) {
const location = `${url}:${line}${col ? ':' + col : ''}`;
result = result ? `${result} at ${location}` : `Error at ${location}`;
}
if (!result && fallbackMessage) {
result = fallbackMessage;
}
return result;
}
function sendToServer(data) {
// 采樣:僅上報 10% 的錯誤,可根據(jù)需要調(diào)整
if (Math.random() > 0.1) return;
const url = 'https://your-monitor-server.com/api/error';
const body = JSON.stringify({
...data,
userAgent: navigator.userAgent,
timestamp: Date.now(),
page: window.location.href
});
if (navigator.sendBeacon) {
navigator.sendBeacon(url, body);
} else {
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body,
keepalive: true
}).catch(() => {});
}
}
window.onerror = function (message, source, lineno, colno, error) {
const errorStr = formatError(error, message, source, lineno, colno);
sendToServer({
type: 'onerror',
rawMessage: message,
stack: errorStr,
url: source,
line: lineno,
column: colno
});
};
window.addEventListener('unhandledrejection', function (event) {
const reason = event.reason;
const errorStr = formatError(reason, 'Unhandled Rejection');
sendToServer({
type: 'unhandledrejection',
stack: errorStr
});
event.preventDefault();
});
})();
進階考慮
1. 去重與聚合
大量相同錯誤重復(fù)上報會浪費資源??梢栽谇岸司彺孀罱蠄蟮腻e誤指紋(如 error.stack 的哈希),短時間內(nèi)相同的錯誤不再發(fā)送。
2. 錯誤采樣
對于高流量的應(yīng)用,可以設(shè)置采樣率,只上報一部分錯誤,減輕服務(wù)器壓力。
3. 附加上下文
除了錯誤信息,還可以記錄用戶的登錄狀態(tài)、操作路徑、API 請求參數(shù)等,幫助復(fù)現(xiàn)問題。
4. 跨域腳本的堆棧
如果引用了 CDN 上的腳本,錯誤堆棧中可能只有 Script error. 而沒有詳細(xì)信息。需要為腳本添加 crossorigin="anonymous" 屬性,并確保服務(wù)器響應(yīng)頭包含 Access-Control-Allow-Origin。
總結(jié)
本文從 console.log(error) 的底層原理出發(fā),設(shè)計了一個兼容所有瀏覽器的錯誤格式化函數(shù),并結(jié)合 window.onerror 和 unhandledrejection 實現(xiàn)了全局異常捕獲與上報。這個方案能夠像原生控制臺一樣輸出完整的錯誤堆棧,同時處理了各種邊界情況(非 Error 對象、舊版 IE、循環(huán)引用等)。將此模塊集成到項目中,你就擁有了一個可靠的前端監(jiān)控基礎(chǔ),為后續(xù)的故障排查和數(shù)據(jù)分析奠定堅實的基礎(chǔ)。
前端異常監(jiān)控并非一勞永逸,還需要不斷優(yōu)化上報策略、豐富上下文信息,以及結(jié)合后端分析工具形成閉環(huán)。但至少,從今天開始,你不再對用戶的錯誤一無所知。
到此這篇關(guān)于前端JS異常捕獲與統(tǒng)一格式化的完整指南的文章就介紹到這了,更多相關(guān)前端異常捕獲與統(tǒng)一格式化內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

