前端错误追踪实战:从捕获到分析的完整方案
先上核心代码,别整那些虚的
项目上线后用户反馈“页面打不开”“点按钮没反应”,但你本地跑得飞起——这时候,错误追踪就是救命稻草。我一开始也懒得搞,直到某天半夜被运营电话叫醒:“线上支付失败了!” 打开控制台一片空白,连错在哪都不知道。折腾半天发现是某个第三方 SDK 抛了异常,但没被捕获。从那以后,我给自己定规矩:新项目必须接入错误追踪。
亲测有效的方式是用 try...catch + 全局监听 + 上报服务。下面这段代码,我用了三年,改过七八次,现在基本稳定:
// 错误上报函数
function reportError(error, info = {}) {
const payload = {
message: error.message || String(error),
stack: error.stack,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
...info
};
// 发送错误(这里用 fetch,实际项目建议用 navigator.sendBeacon)
fetch('https://jztheme.com/api/error-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).catch(() => {
// 上报失败就默默放弃,别再抛错
});
}
// 全局未捕获异常监听
window.addEventListener('error', (event) => {
reportError(event.error, { type: 'global-error' });
});
// Promise 拒绝未处理监听
window.addEventListener('unhandledrejection', (event) => {
reportError(event.reason, { type: 'unhandled-rejection' });
event.preventDefault(); // 防止控制台报红(可选)
});
这段代码能捕获绝大多数运行时错误。注意,fetch 在上报失败时不能抛错,否则会陷入无限循环——我踩过这个坑,直接把服务器打挂了。
异步错误、Promise 拒绝,一个都不能少
很多人以为加个 window.onerror 就万事大吉,结果 Promise 里 reject 的错误根本收不到。上面代码里特意加了 unhandledrejection 监听,这是关键。但要注意:一旦你手动 .catch() 了 Promise,它就不会触发这个事件。所以团队要统一规范:要么全局兜底,要么每个 async 函数都 try-catch。
比如这个常见场景:
async function fetchData() {
try {
const res = await fetch('/api/data');
if (!res.ok) throw new Error('API failed');
return res.json();
} catch (err) {
// 如果这里不 rethrow,unhandledrejection 就收不到
// 所以要么在这里上报,要么让错误继续抛
reportError(err, { context: 'fetchData' });
throw err; // 或者返回默认值,看业务需求
}
}
建议:在业务层尽量不要吞掉错误,除非你明确知道后果。否则追踪系统里永远看不到这类问题。
踩坑提醒:这三点一定注意
- 跨域脚本的错误全是 Script error. —— 这是最烦的。解决方法有两个:一是给 script 标签加
crossorigin属性,二是确保 CDN 返回的响应头包含Access-Control-Allow-Origin: *。我之前用的静态资源托管没配 CORS,所有错误都变成 “Script error.”,排查了两天才定位到是跨域问题。 - 不要在错误处理里再抛错 —— 上面代码里
reportError用了.catch(() => {})就是这个原因。曾经有个同事在上报逻辑里用了未定义的变量,结果每次出错都触发新错误,雪崩式上报,直接把监控后台打爆。 - 用户隐私和敏感信息 —— 别把用户的 token、手机号、银行卡号通过错误堆栈传出去。有些框架会把组件 props 打印到错误信息里,务必在上报前过滤。比如 React 的错误边界,有时候会把 state 打出来,得手动清洗。
这个场景最好用:结合 Source Map 还原真实代码
生产环境代码都是压缩混淆的,堆栈信息基本没法看。这时候必须配合 Source Map。做法是:构建时生成 map 文件,部署到私有服务器(千万别公开访问!),然后在错误上报平台配置解析规则。
以 Sentry 为例(虽然我后面换了自研方案,但原理一样),它支持自动上传 sourcemap 并解析。如果你自己搭服务,可以这样处理:
// 假设你的错误堆栈是这样的:
// at a (https://jztheme.com/app.12345.js:1:2345)
// 你需要根据文件名和行列号,调用 sourcemap 解析库
// 例如使用 source-map 库(Node.js 环境)
const fs = require('fs');
const { SourceMapConsumer } = require('source-map');
async function parseStack(stack) {
const consumer = await new SourceMapConsumer(
fs.readFileSync('app.12345.js.map', 'utf8')
);
const original = consumer.originalPositionFor({
line: 1,
column: 2345
});
// 得到 { source: 'src/utils.js', line: 42, column: 10, name: 'handleClick' }
}
不过说实话,自建 sourcemap 解析挺麻烦的,小团队建议直接用 Sentry 或 Fundebug 这类服务,省心。我之前为了省那点钱自己搞,结果花了一周时间调试,最后还是切回了付费方案——时间比钱贵。
高级技巧:给错误加“上下文”
光知道“哪里错了”不够,还得知道“为什么错”。比如用户点击“提交订单”时出错,你得知道他当时填了什么地址、选了什么商品。这时候可以在关键操作前埋点:
let currentContext = {};
function setContext(key, value) {
currentContext[key] = value;
}
// 在路由跳转、表单提交等地方设置上下文
router.beforeEach((to) => {
setContext('route', to.path);
});
document.getElementById('submit-btn').addEventListener('click', () => {
setContext('form-data', {
address: document.getElementById('address').value,
product: selectedProduct
});
// ...提交逻辑
});
// 修改上报函数,带上上下文
function reportError(error, info = {}) {
const payload = {
// ...原有字段
context: { ...currentContext },
...info
};
// ...
}
注意:context 要定期清理,避免内存泄漏。比如页面 unload 时清空,或者限制最大条目数。
最后说点实在的
错误追踪不是一劳永逸的事。我见过太多团队接了 Sentry 就不管了,结果报警邮件堆成山,没人看。建议:初期每天花 10 分钟看下错误面板,快速修复高频问题;等稳定了,再设阈值告警(比如同一错误 1 小时超过 100 次才通知)。
另外,别指望 100% 捕获所有错误。有些浏览器兼容性问题、极端网络中断,可能连上报都发不出去。但只要覆盖 90% 的 JS 异常,就能解决大部分线上问题。
以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如和性能监控联动、自动化创建 Jira 工单),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流——尤其是怎么低成本做 sourcemap 管理,我还在找更好的方案。

暂无评论