跳到主要内容前端异常捕获与统一格式化:从 console.log 到服务端上报 | 极客日志JavaScript大前端
前端异常捕获与统一格式化:从 console.log 到服务端上报
前端异常监控是保障应用稳定性的关键。通过 window.onerror 和 unhandledrejection 可捕获全局错误,但不同浏览器参数差异大,直接上报往往信息不全。深入分析 console.log 底层原理,设计兼容多浏览器的错误格式化函数,优先提取 stack 属性,降级处理非 Error 对象及旧版 IE 场景。结合 sendBeacon 实现低侵入上报,并探讨采样、去重及跨域堆栈等进阶策略,帮助开发者构建可靠的前端监控体系。
游戏玩家3 浏览 前端异常捕获与统一格式化:从 console.log 到服务端上报
在前端开发中,异常监控是保证应用稳定性的重要一环。当用户遇到页面白屏、功能不可用等问题时,如果能及时收集到详细的错误信息(包括堆栈、行列号、浏览器环境等),就能快速定位并修复 bug。浏览器提供了 window.onerror 和 unhandledrejection 两个全局事件,分别用于捕获未处理的 JavaScript 异常和未捕获的 Promise 拒绝。然而,不同浏览器对这些事件的参数支持存在差异,错误对象的格式也各不相同,如何编写一个兼容所有浏览器、并能像 console.log(error) 那样输出完整堆栈的格式化函数,是搭建前端监控系统的第一步。
为什么需要统一格式化?
当你在控制台直接执行 console.log(new Error('something wrong')) 时,浏览器会打印出类似这样的信息:
Error: something wrong at <anonymous>:1:13 at ...
但如果使用 window.onerror 捕获,你拿到的参数可能只有消息、脚本 URL、行号、列号和一个可选的 error 对象。这些参数组合起来未必能还原出完整的堆栈。此外,unhandledrejection 的 reason 可能是任意类型(字符串、对象、Error 实例等),如何安全地提取信息并拼接成可读的字符串,也需要仔细处理。
一个优秀的异常上报方案应该做到:
- 完整性:尽可能包含错误名称、消息、调用堆栈、发生位置(文件、行号、列号)。
- 兼容性:支持所有主流浏览器(包括 IE9+)。
- 健壮性:处理循环引用、非 Error 对象等特殊情况,避免二次异常。
- 一致性:最终上报的字符串格式统一,便于后端解析或搜索。
console.log(error) 的底层原理
在深入实现之前,我们先了解一下浏览器是如何打印错误对象的。以 Chrome 的 V8 引擎为例:
console.log 接收一个对象后,会调用该对象的 [Symbol.toStringTag] 或自定义的 inspect 方法(DevTools 扩展)。对于 Error 对象,V8 内部会检查其是否有 stack 属性。
error.stack 是一个非标准但所有现代浏览器都支持的属性,它包含了当前调用栈的快照。这个堆栈字符串的生成依赖于 Error.captureStackTrace(Node.js 中)或运行时自动收集的调用帧。
- 如果
error.stack 存在,浏览器直接输出该字符串;否则,退而使用 error.toString()(通常是 "Error: message" 的形式)。
因此,要获得与 console.log 相同的输出,我们只需在全局事件中尽量获取到 error.stack 即可。当无法获取 stack 时,再根据事件参数手动拼接位置信息。
统一错误格式化函数
下面是一个健壮的 formatError 函数,它接受任意类型的错误值以及可选的 URL、行号、列号,返回格式化的错误字符串。
() {
result = ;
(error && error === ) {
( error. === ) {
result = error.;
}
( error. === ) {
name = error. || ;
result = ;
}
{
{
result = .(error, , );
} (e) {
result = (error);
}
}
} {
result = (error);
}
hasLineInfo = .(result);
(!hasLineInfo && url && line) {
location = ;
result = result ? : ;
}
(!result && fallbackMessage) {
result = fallbackMessage;
}
result;
}
string
fallbackMessage
@param
string
@param
number
@param
number
@returns
string
function
formatError
error, fallbackMessage, url, line, col
let
''
if
typeof
'object'
if
typeof
stack
'string'
stack
else
if
typeof
message
'string'
const
name
'Error'
`${name}: ${error.message}`
else
try
JSON
stringify
null
2
catch
String
else
String
const
/:\d+/
test
if
const
`${url}:${line}${col ? ':' + col : ''}`
`${result} at ${location}`
`Error at ${location}`
if
return
关键点说明
- 优先使用
error.stack:只要错误对象有 stack 属性,就直接使用它,因为 stack 已经包含了最完整的调用链和位置信息。
- 降级使用
name 和 message:如果对象是 Error 实例但 stack 可能被篡改或不存在,则拼接 name: message。
- JSON 序列化兜底:对于普通对象(如
{ code: 500, msg: 'fail' }),尝试用 JSON.stringify 展示其结构,并捕获循环引用异常。
- 附加行列号:当最终字符串中没有明显的数字位置(如
:10)且外部提供了 URL 和行号时,将位置信息附加到末尾。这可以弥补某些场景下 error.stack 缺失行列的不足。
- fallbackMessage 参数:当 error 为
undefined 或空值时,可以传入默认消息,例如 'Unhandled Rejection'。
全局监听器:window.onerror 和 unhandledrejection
有了格式化函数,我们就可以在全局事件中调用它,并将结果上报。
window.onerror
window.onerror = function(message, source, lineno, colno, error) {
const errorStr = formatError(error, message, source, lineno, colno);
sendToServer({
type: 'onerror',
message: message,
stack: errorStr,
url: source,
line: lineno,
column: colno,
userAgent: navigator.userAgent,
timestamp: Date.now()
});
};
注意:旧版 IE(<=10)不会传递 error 参数,此时 error 为 undefined,我们的 formatError 会使用 fallbackMessage(即 message)和行列号来构造字符串。
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()
});
event.preventDefault();
});
event.reason 可以是任何类型,我们的 formatError 已经做了充分处理。
上报函数实现
最简单的上报可以通过 navigator.sendBeacon 或 fetch 发送到后端接口。为了不影响用户体验,建议使用 sendBeacon,它会在页面卸载时也能确保请求发出。
function sendToServer(data) {
if (Math.random() > 0.1) return;
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
}).catch(() => {});
}
}
兼容性深度解析
不同浏览器的 window.onerror 参数
| 浏览器 | message | source | lineno | colno | error |
|---|
| Chrome / Firefox / Safari / Edge (现代) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| IE 10+ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️(但可能为 null) |
| IE 9- | ✔️ | ✔️ | ✔️ | ❌ | ❌ |
我们的 formatError 能够适应以上所有情况:当 error 不存在时,利用 message、source、lineno 构造一个简化版本。
堆栈格式差异
不同浏览器生成的 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)
这些格式差异通常不影响可读性,我们的格式化函数直接保留原始 stack,不进行解析和重组,以保证信息不丢失。
完整示例代码
(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) {
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. 去重与聚合
大量相同错误重复上报会浪费资源。可以在前端缓存最近上报的错误指纹(如 error.stack 的哈希),短时间内相同的错误不再发送。
2. 错误采样
对于高流量的应用,可以设置采样率,只上报一部分错误,减轻服务器压力。
3. 附加上下文
除了错误信息,还可以记录用户的登录状态、操作路径、API 请求参数等,帮助复现问题。
4. 跨域脚本的堆栈
如果引用了 CDN 上的脚本,错误堆栈中可能只有 Script error. 而没有详细信息。需要为脚本添加 crossorigin="anonymous" 属性,并确保服务器响应头包含 Access-Control-Allow-Origin。
总结
本文从 console.log(error) 的底层原理出发,设计了一个兼容所有浏览器的错误格式化函数,并结合 window.onerror 和 unhandledrejection 实现了全局异常捕获与上报。这个方案能够像原生控制台一样输出完整的错误堆栈,同时处理了各种边界情况(非 Error 对象、旧版 IE、循环引用等)。将此模块集成到项目中,你就拥有了一个可靠的前端监控基础,为后续的故障排查和数据分析奠定坚实的基础。
前端异常监控并非一劳永逸,还需要不断优化上报策略、丰富上下文信息,以及结合后端分析工具形成闭环。但至少,从今天开始,你不再对用户的错误一无所知。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online