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

