前端异常捕获与统一格式化:从 console.log(error) 到服务端上报

前端异常捕获与统一格式化:从 console.log(error) 到服务端上报
🧑 博主简介ZEEKLOG博客专家「历代文学网」(公益文学网,PC端可以访问:https://lidaiwenxue.com/#/?__c=1000,移动端可关注公众号 “ 心海云图 ” 微信小程序搜索“历代文学”)总架构师,首席架构师,也是联合创始人!16年工作经验,精通Java编程高并发设计分布式系统架构设计Springboot和微服务,熟悉LinuxESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
🤝商务合作:请搜索或扫码关注微信公众号 “ 心海云图


在这里插入图片描述

前端异常捕获与统一格式化:从 console.log(error) 到服务端上报

引言

在前端开发中,异常监控是保证应用稳定性的重要一环。当用户遇到页面白屏、功能不可用等问题时,如果能及时收集到详细的错误信息(包括堆栈、行列号、浏览器环境等),就能快速定位并修复 bug。浏览器提供了 window.onerrorunhandledrejection 两个全局事件,分别用于捕获未处理的 JavaScript 异常和未捕获的 Promise 拒绝。然而,不同浏览器对这些事件的参数支持存在差异,错误对象的格式也各不相同,如何编写一个兼容所有浏览器、并能像 console.log(error) 那样输出完整堆栈的格式化函数,是搭建前端监控系统的第一步。

本文将带你深入理解 console.log(error) 的底层实现,并给出一个通用的错误格式化方案,最后演示如何将格式化后的异常信息上报到后端。

为什么需要统一格式化?

当你在控制台直接执行 console.log(new Error('something wrong')) 时,浏览器会打印出类似这样的信息:

Error: something wrong at <anonymous>:1:13 at ... 

但如果使用 window.onerror 捕获,你拿到的参数可能只有消息、脚本 URL、行号、列号和一个可选的 error 对象。这些参数组合起来未必能还原出完整的堆栈。此外,unhandledrejectionreason 可能是任意类型(字符串、对象、Error 实例等),如何安全地提取信息并拼接成可读的字符串,也需要仔细处理。

一个优秀的异常上报方案应该做到:

  • 完整性:尽可能包含错误名称、消息、调用堆栈、发生位置(文件、行号、列号)。
  • 兼容性:支持所有主流浏览器(包括 IE9+)。
  • 健壮性:处理循环引用、非 Error 对象等特殊情况,避免二次异常。
  • 一致性:最终上报的字符串格式统一,便于后端解析或搜索。

console.log(error) 的底层原理

在深入实现之前,我们先了解一下浏览器是如何打印错误对象的。以 Chrome 的 V8 引擎为例:

  1. console.log 接收一个对象后,会调用该对象的 [Symbol.toStringTag] 或自定义的 inspect 方法(DevTools 扩展)。对于 Error 对象,V8 内部会检查其是否有 stack 属性。
  2. error.stack 是一个非标准但所有现代浏览器都支持的属性,它包含了当前调用栈的快照。这个堆栈字符串的生成依赖于 Error.captureStackTrace(Node.js 中)或运行时自动收集的调用帧。
  3. 如果 error.stack 存在,浏览器直接输出该字符串;否则,退而使用 error.toString()(通常是 "Error: message" 的形式)。

因此,要获得与 console.log 相同的输出,我们只需在全局事件中尽量获取到 error.stack 即可。当无法获取 stack 时,再根据事件参数手动拼接位置信息。

统一错误格式化函数

下面是一个健壮的 formatError 函数,它接受任意类型的错误值以及可选的 URL、行号、列号,返回格式化的错误字符串。

/** * 将任意错误值格式化为包含堆栈信息的字符串 * @param {*} error - 错误对象或任意值 * @param {string} fallbackMessage - 当无法获取有效信息时的备选消息 * @param {string} [url] - 发生错误的脚本URL(从onerror获取) * @param {number} [line] - 行号(从onerror获取) * @param {number} [col] - 列号(从onerror获取) * @returns {string} 格式化后的错误字符串 */functionformatError(error, fallbackMessage, url, line, col){let result ='';// 情况1:error 是对象类型,尝试提取 stack 或 messageif(error &&typeof error ==='object'){// 优先使用 stack(包含完整的调用堆栈)if(typeof error.stack ==='string'){ result = error.stack;}// 其次使用标准 error 属性(name 和 message)elseif(typeof error.message ==='string'){const name = error.name ||'Error'; result =`${name}: ${error.message}`;}// 否则尝试 JSON 序列化(避免循环引用)else{try{ result =JSON.stringify(error,null,2);}catch(e){// 序列化失败(如循环引用),使用默认字符串转换 result =String(error);}}}else{// 原始类型直接转为字符串 result =String(error);}// 情况2:结果中不包含行列信息(如只拿到 message),但通过 onerror 获得了具体位置// 简单判断堆栈中是否已有类似 ":数字" 的行号标记const hasLineInfo =/:\d+/.test(result);if(!hasLineInfo && url && line){const location =`${url}:${line}${col ?':'+ col :''}`; result = result ?`${result} at ${location}`:`Error at ${location}`;}// 情况3:仍然没有有效内容,使用 fallbackMessageif(!result && fallbackMessage){ result = fallbackMessage;}return result;}

关键点说明

  • 优先使用 error.stack:只要错误对象有 stack 属性,就直接使用它,因为 stack 已经包含了最完整的调用链和位置信息。
  • 降级使用 namemessage:如果对象是 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 函数)sendToServer({type:'onerror',message: message,stack: errorStr,url: source,line: lineno,column: colno,userAgent: navigator.userAgent,timestamp: Date.now()});// 返回 true 可以阻止浏览器默认处理(如控制台打印错误)// return true;};

注意:旧版 IE(<=10)不会传递 error 参数,此时 errorundefined,我们的 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.sendBeaconfetch 发送到后端接口。为了不影响用户体验,建议使用 sendBeacon,它会在页面卸载时也能确保请求发出。

functionsendToServer(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 参数

浏览器messagesourcelinenocolnoerror
Chrome / Firefox / Safari / Edge (现代)✔️✔️✔️✔️✔️
IE 10+✔️✔️✔️✔️✔️(但可能为 null)
IE 9-✔️✔️✔️

我们的 formatError 能够适应以上所有情况:当 error 不存在时,利用 messagesourcelineno 构造一个简化版本。

堆栈格式差异

不同浏览器生成的 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,不进行解析和重组,以保证信息不丢失。

完整示例代码

将上述片段整合,得到一个完整的监控模块:

// error-monitor.js(function(){'use strict';functionformatError(error, fallbackMessage, url, line, col){let result ='';if(error &&typeof error ==='object'){if(typeof error.stack ==='string'){ result = error.stack;}elseif(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;}functionsendToServer(data){// 采样:仅上报 10% 的错误,可根据需要调整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.onerrorunhandledrejection 实现了全局异常捕获与上报。这个方案能够像原生控制台一样输出完整的错误堆栈,同时处理了各种边界情况(非 Error 对象、旧版 IE、循环引用等)。将此模块集成到项目中,你就拥有了一个可靠的前端监控基础,为后续的故障排查和数据分析奠定坚实的基础。

前端异常监控并非一劳永逸,还需要不断优化上报策略、丰富上下文信息,以及结合后端分析工具形成闭环。但至少,从今天开始,你不再对用户的错误一无所知。

Read more

Flutter for OpenHarmony: Flutter 三方库 cached_query 为鸿蒙应用打造高性能声明式数据缓存系统(前端缓存终极方案)

Flutter for OpenHarmony: Flutter 三方库 cached_query 为鸿蒙应用打造高性能声明式数据缓存系统(前端缓存终极方案)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 应用开发时,网络请求的响应速度直接决定了用户体验(体验 UX)。如果用户每次切换页面都必须等待加载动画,应用会显得非常低级。我们不仅需要处理异步数据请求,更需要一套精密的机制来解决以下痛点: 1. 自动缓存:第二次访问时应瞬间展示历史数据。 2. 过期失效(Stale-while-revalidate):在展示旧数据的同时,后台静默拉取新数据。 3. 无限滚动:简单地处理分页与数据追加内容逻辑。 cached_query 是一个类似于 Web 端 React Query 的 Dart 状态管理库。它专注于数据获取与同步,让你的鸿蒙应用具备顶级的数据缓存表现。 一、核心缓存驱动机制 cached_query 在内存与数据源之间建立了一层“智能感知”缓存。 数据过期/缺失 返回新数据 发射流

By Ne0inhk
【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题

【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题

【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题 在开发 Web 应用时,尤其是集成了 Unity WebGL 内容的页面,遇到一个问题:当 Unity WebGL 渲染内容嵌入到一个 Tab 中时,切换 Tab 后画面会变黑,直到用户点击黑屏区域,才会恢复显示。 这个问题通常是因为 Unity 渲染在 Tab 切换时被暂停或未能获得焦点所致。 在本文中,我们将介绍如何在使用 Layui 框架时,通过监听 Tab 切换事件并强制 Unity WebGL 渲染恢复,来解决这一问题。 1. 问题描述 当 Unity WebGL 内容嵌入到页面中的多个

By Ne0inhk
JavaScript 中 var、let、const 的核心区别与实战应用

JavaScript 中 var、let、const 的核心区别与实战应用

要理解 const、var、let 的区别,我们可以从 作用域、变量提升、可重复声明、可修改性 这几个核心维度展开,这些也是新手最容易混淆的点。 一、核心概念铺垫 首先明确两个基础概念,能帮你更好理解区别: * 函数作用域:变量只在声明它的函数内部可访问(var 是函数作用域)。 * 块级作用域:变量只在声明它的 {} 内部可访问(let/const 是块级作用域,{} 包括 if/for/while/ 普通代码块)。 * 变量提升:JS 引擎在执行代码前,会把变量声明 “提升” 到当前作用域顶部(但赋值不会提升)。 二、逐个拆解 + 对比 1. var(ES5 语法) var 是 ES5 中声明变量的方式,特性如下:

By Ne0inhk
从 XMLHttpRequest 到 Fetch API:现代前端网络请求的演进与迁移指南

从 XMLHttpRequest 到 Fetch API:现代前端网络请求的演进与迁移指南

🧑 博主简介:ZEEKLOG博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可关注公众号 “ 心海云图 ” 微信小程序搜索“历代文学”)总架构师,16年工作经验,精通Java编程,高并发设计,分布式系统架构设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。 🤝商务合作:请搜索或扫码关注微信公众号 “ 心海云图 ” 从 XMLHttpRequest 到 Fetch API:现代前端网络请求的演进与迁移指南 引言:为什么我们需要新的网络请求方案? 在前端开发领域,XMLHttpRequest (XHR) 长期统治着浏览器端的网络请求。然而,随着 Web

By Ne0inhk