浅谈前端错误处理
发布于 6 年前 作者 ixlei 2464 次浏览 来自 分享

某一天用户反馈打开的页面白屏幕,怎么定位到产生错误的原因呢?日常某次发布怎么确定发布会没有引入bug呢?此时捕获到代码运行的bug并上报是多么的重要。

既然捕获错误并上报是日常开发中不可缺少的一环,那怎么捕获到错误呢?万能的**try…catch**


try{

throw new Error()

} catch(e) {

// handle error

}

看上去错误捕获是多么的简单,然而下面的场景下就不能捕获到了


try {

setTimeout(() => {

throw new Error('error')

})

} catch (e) {

// handle error

}

你会发现上面的例子中的错误不能正常捕获,看来错误捕获并不是这样简单**try…catch**就能搞定,当然你也可以为异步函数包裹一层**try…catch**来处理。

浏览器中,**window.onerror**来捕获你的错误


window.onerror = function (msg, url, row, col, error) {

console.log('error');

console.log({

msg, url, row, col, error

})

};

捕获到错误后就可以将错误上报,上报方式很简单,你可以通过创建简单的**img**,通过**src**指定上报的地址,当然为了避免上报发送过多的请求,可以对上报进行合并,合并上报。可以定时将数据进行上报到服务端。

但但你去看错误上报的信息的时候,你会发现一些这样的错误**Script error**

因为浏览器的同源策略,对于不同域名的错误,都抛出了**Script error**,怎么解决这个问题呢?特别是现在基本上js资源都会放在cdn上面。

解决方案

1:所有的资源都放在同一个域名下。但是这样也会存在问题是不能利用cdn的优势。

2:增加跨域资源支持,在cdn 上增加支持主域的跨域请求支持,在script 标签加**crossorigin**属性

在使用Promise过程中,如果你没有catch,那么可以这样来捕获错误

window.addEventListener("unhandledrejection", function(err, promise) { 
    // handle error here, for example log   
});

如何在NodeJs中捕获错误

NodeJs中的错误捕获很重要,因为处理不当可能导致服务雪崩而不可用。当然了不仅仅知道如何捕获错误,更应该知道如何避免某些错误。

  • 当你写一个函数的时候,你也许曾经思考过当函数执行的时候出现错误的时候,我是应该直接抛出throw,还是使用callback或者event emitter还是其它方式分发错误呢?

  • 我是否应该检查参数是否是正确的类型,是不是null

  • 如果参数不符合的时候,你怎么办呢?抛出错误还是通过callback等方式分发错误呢?

  • 如果保存足够的错误来复原错误现场呢?

  • 如果去捕获一些异常错误呢?try…catch还是domain

操作错误VS编码错误

1. 操作错误

操作错误往往发生在运行时,并非由于代码bug导致,可能是由于你的系统内存用完了或者是由于文件句柄用完了,也可能是没有网络了等等

2.编码错误

编码错误那就比较容易理解了,可能是undefined却当作函数调用,或者返回了不正确的数据类型,或者内存泄露等等

处理操作错误

  • 你可以记录一下错误,然后什么都不做

  • 你也可以重试,比如因为链接数据库失败了,但是重试需要限制次数

  • 你也可以将错误告诉前端,稍后再试

  • 也许你也可以直接处理,比如某个路径不存在,则创建该路径

处理编码错误

错误编码是不好处理的,因为是由于编码错误导致的。好的办法其实重启该进程,因为

  • 你不确定某个编码错误导致的错误会不会影响其它请求,比如建立数据库链接错误由于编码错误导致不能成功,那么其它错误将导致其它的请求也不可用

  • 或许在错误抛出之前进行IO操作,导致IO句柄无法关闭,这将长期占有内存,可能导致最后内存耗尽整个服务不可用。

  • 上面提到的两点其实都没有解决问题根本,应该在上线前做好测试,并在上线后做好监控,一旦发生类似的错误,就应该监控报警,关注并解决问题

如何分发错误

  • 在同步函数中,直接throw出错误

  • 对于一些异步函数,可以将错误通过callback抛出

  • async/await可以直接使用try…catch捕获错误

  • EventEmitter抛出error事件

NodeJs的运维

一个NodeJs运用,仅仅从码层面是很难保证稳定运行的,还要从运维层面去保障。

多进程来管理你的应用

单进程的nodejs一旦挂了,整个服务也就不可用了,所以我萌需要多个进程来保障服务的可用,某个进程只负责处理其它进程的启动,关闭,重启。保障某个进程挂掉后能够立即重启。

可以参考TSW中多进程的设计。master负责对worker的管理,worker和master保持这心跳监测,一旦失去,就立即重启之。

domain
process.on('uncaughtException', function(err) {
    console.error('Error caught in uncaughtException event:', err);
});
process.on('unhandleRejection', function(err) {
  // TODO
})

上面捕获nodejs中异常的时候,可以说是很暴力。但是此时捕获到异常的时候,你已经失去了此时的上下文,这里的上下文可以说是某个请求。假如某个web服务发生了一些异常的时候,还是希望能够返回一些兜底的内容,提升用户使用体验。比如服务端渲染或者同构,即使失败了,也可以返回个静态的html,走降级方案,但是此时的上下文已经丢失了。没有办法了。

function domainMiddleware(options) {
    return async function (ctx, next) {
        const request = ctx.request;
        const d = process.domain || domain.create();
        d.request = request;
        let errHandler = (err) => {
            ctx.set('Content-Type', 'text/html; charset=UTF-8');
            ctx.body = options.staticHtml;
        };
        d.on('error', errHandler);
        d.add(ctx.request);
        d.add(ctx.response);
        try {
            await next();
        } catch(e) {
            errHandler(e)
        }
    }

上面是一个简单的koa2的domain的中间件,利用domain监听error事件,每个请求的Request, Response对象在发生错误的时候,均会触发error 事件,当发生错误的时候,能够在有上下文的基础上,可以走降级方案。

如何避免内存泄露

内存泄漏很常见,特别是前端去写后端程序,闭包运用不当,循环引用等都会导致内存泄漏。

  • 不要阻塞Event Loop的执行,特别是大循环或者IO同步操作

    for ( var i = 0; i < 10000000; i++ ) {
        var user       = {};
        user.name  = 'outmem';
        user.pass  = '123456';
        user.email = 'outmem[@outmem](/user/outmem).com';
    }
    

    上面的很长的循环会导致内存泄漏,因为它是一个同步执行的代码,将在进程中执行,V8在循环结束的时候,是没办法回收循环产生的内存的,这会导致内存一直增长。还有可能原因是,这个很长的执行,阻塞了node进入下一个Event loop, 导致队列中堆积了太多等待处理已经准备好的回调,进一步加剧内存的占用。那怎么解决呢?

    可以利用setTimeout将操作放在下一个loop中执行,减少长循环,同步IO对进程的阻.阻塞下一个loop 的执行,也会导致应用的性能下降

  • 模块的私有变量和方法都会常驻在内存中

var leakArray = [];   
exports.leak = function () {  
  leakArray.push("leak" + Math.random());  
};

在node中require一个模块的时候,最后都是形成一个单例,也就是只要调用该函数一下,函数内存就会增长,闭包不会被回收,第二是leak方法是一个私有方法,这个方法也会一直存在内存。加入每个请求都会调用一下这个方法,那么内存一会就炸了。

这样的场景其实很常见

// main.js
function Main() {
  this.greeting = 'hello world';
}
module.exports = Main;
var a = require('./main.js')();
var b = require('./main.js')();
a.greeting = 'hello a';
console.log(a.greeting); // hello a
console.log(b.greeting); // hello a

require得到是一个单例,在一个服务端中每一个请求执行的时候,操作的都是一个单例,这样每一次执行产生的变量或者属性都会一直挂在这个对象上,无法回收,占用大量内存。

其实上面可以按照下面的调用方式来调用,每次都产生一个实例,用完回收。

var a = new require('./main.js');
// TODO

有的时候很难避免一些可能产生内存泄漏的问题,可以利用vm每次调用都在一个沙箱环境下调用,用完回收调。

  • 最后就是避免循环引用了,这样也会导致无法回收

招纳贤士

今日头条长期大量招聘前端工程师,可选北京、深圳、上海、厦门等城市。欢迎投递简历到 tcscyl@gmail.com / yanglei.yl@bytedance.com

回到顶部