本文主要从前端开发者的角度谈一谈大多数前端开发者都会遇到的js错误,对错误产生的缘由、发生阶段,以及如何应对错误进行分析、概括和总结,但愿获得一些有益的结论用来指导平常开发工做。javascript
对于Java来讲错误和异常是两个相近可是不一样的概念,而在JavaScript中能够认为错误和异常是等同的,js里只有Error关键字,并没有Exception关键字。下文指的js错误也指一般理解的js异常。前端
一般是非程序设计的缘由致使的错误,大部分是发生在应用环境中的外部错误,好比硬件故障致使的I/O Error、网络不稳定致使的Network Error,调用不被信任的外部方法,DOM操做,使用new Image、new FileReader加载资源。错误能够被忽略或者捕获,代码能够经过预设错误处理流程,例如使用了重试机制的代码能够经过重试有可能让程序恢复到正常状态。java
一般是程序设计的缘由致使的计算机程序或系统中的缺陷,可以引起错误或意外结果,或使程序或系统以非预期方式运行。一般没法继续和恢复,须要程序员进入程序而且修改代码来修复。程序员
因为JavaScript语言解释型的特性,js错误发生在运行时,这一点和编译型语言相比错误更加难以发现。幸运的是技术发展到今天已经有很是多成熟的工具好比Eslint、IDE的代码检查,能够帮助咱们在早期不须要运行程序的阶段发现错误。还有一些JavaScript语言的超集语言,为语言添加了可选的静态类型,也能够帮助在早期发现错误。程序运行开始后,在进行用户交互以前,一些语法错误,常见的好比Uncaught SyntaxError是很是容易发现的。而剩下的那些,基本上须要经过用户交互来触发,较难以发现,本文着重讨论这部分错误。bash
若是判断程序当前位置可能会发生错误,不捕获错误是一种消极的应对方式。依赖全局window.onerror错误监听可以获取未捕获的错误信息。服务器
若是捕获错误后立刻抛出,抛出的error对象就是原来的对象,至关于啥也没干,等同于不捕获错误。通常来讲采用这种应对方式时都会干点什么,好比抛出一个自定义的错误,而不是原始错误对象。网络
要当心不处理不抛出意味着全局window.onerror错误监听也没法获取到该错误信息,通常来讲这种应对方式用在一些不影响程序主流程的错误处理上,好比调用DOM节点的focus方法,但仍是须要注释合理的理由。 捕获错误后静默处理的示例:并发
try {
obj[0].focus();
} catch (e) {
// IE8 can throw "Can't move focus to the control because it is invisible, // not enabled, or of a type that does not accept the focus." for all kinds of
// reasons that are too expensive and fragile to test.
}
复制代码
一般会在处理方法体中使用错误日志上报、失效保护、重试恢复等技术。失效保护即降级处理,举一个比较简明的例子,try { a = JSON.parse(b); } catch (e) { a = {}; }
异步
这个问题缺少前提,究竟是捕获了不处理仍是未捕获错误。前者已有解答,这里说未捕获错误可能会形成的影响。错误发生后,在出错位置以前的代码已经执行过了,以后的代码再也不执行,这种状况基本上就是bug了。因为js的并发模型与事件循环机制,若是在出错位置以前执行过异步代码,好比setTimeout、new Promise,异步代码中的回调函数仍然会在“执行栈”中的全部同步任务执行完毕以后按照回调函数在任务队列里的顺序执行。值得注意的是,利用这种特性,能够将错误包装在一个异步执行的函数中使用异步抛出错误的技术,可以避免阻断错误处以后的代码执行。 异步抛出错误示例:async
const asyncThrowError = (error) => {
setTimeout(function() {
throw error || new Error('异步抛出的异常');
});
};
try {
let a = JSON.parse('{a: a}');
} catch(e) {
// 使用异步抛出异常的技术,依靠window.onerror捕获异常并记录日志
asyncThrowError(e);
}
复制代码
固然是预感程序某一处可能会出现问题的时候啦。具体何时则见仁见智啦,依赖程序员的经验。
人为主动抛出的错误和应用环境中的发生的错误一样会致使错误位置以后的代码没法执行。常见的一种用法是在函数检查传入的参数是否合法,不合法就使程序快速失效(Fail Fast),提醒开发者修复问题。还能够将抛出错误技术用在收集用户填写的不合法的表单数据上,在内层代码中抛出错误,在外层代码中捕获错误并判断错误类型,获取到错误信息。
若是前提得是可恢复类型的错误,在程序中加入重试机制才有意义。可恢复的错误一般是外部缘由致使的,典型例子如网络错误。重试机制根据是否须要用户交互触发可分为自动重试和手动重试。自动重试的一种设计是,经过线性增加的间隔时间或者成指数增加的间隔时间循环重试直到没有错误发生,尤为是指数增加的间隔时间循环重试机制能够避免程序太快将计算机资源占满。手动重试的一个设计例子是,在一些关键业务流程,好比电商场景中的添加到购物车,当接口返回失败时,能够经过让用户手动点击重试按钮触发新的一轮接口调用。 自动重试的一种设计方案:
const sendMesg = (errorResendTimes = 0) => {
console.log(errorResendTimes);
try {
throw 'hahaha';
} catch (e) {
setTimeout(
() => sendMesg(errorResendTimes),
100 * Math.pow(2, errorResendTimes) // 使用指数增加的时间间隔
// 100 * errorResendTimes // 使用线性增加的时间间隔
);
errorResendTimes++;
}
};
复制代码
这个是js中捕获并处理错误的最核心的出发点,有经验的程序员为了不未捕获的错误阻断程序执行形成bug,须要在程序中设置陷阱(使用try/catch语句)捕获错误。当错误发生的时候,使用了错误捕获技术的地方能够防止错误影响当前调用栈以后的代码执行,隔离了错误的影响范围,将影响范围限制在当前函数做用域。错误的降级处理指的是当错误发生后,虽然已使用错误捕获技术隔离了影响范围,可是假如当前调用栈以后的代码仍然依赖当前的执行结果,错误致使执行结果为非预期的数据类型,那么以后的代码使用该执行结果必然又会出错。 错误的降级处理示例:
let a;
try {
a = JSON.parse('{a: a}');
} catch (e) {
a = {};
}
console.log(a.a);
复制代码
这个问题分为两个步骤,首先要获取到错误信息,其次是预处理这些错误信息,一般状况下使用一些技术手段合并、降频,最后再上报日志服务器。这里着重谈如何获取到更全面细致的错误信息。直接看示例:
// 监听js运行时异常
window.onerror = (message, source, lineno, colno, error) => {
var errorMesg = wrapError({message, source, lineno, colno, error});
console.log(message)
sendErrorToServer('js运行时异常:' + errorMesg);
}
// 监听document资源加载异常
window.addEventListener('error', function(e) {
// 过滤非window上捕获的异常
if (e.target === window) {
return;
}
var errorMesg = wrapError(e);
sendErrorToServer('document资源加载异常:' + errorMesg);
}, true);
// 监听未捕获的Promise异常
window.addEventListener('unhandledrejection', function(e) {
var errorMesg = e.reason;
sendErrorToServer('未捕获的Promise异常:' + errorMesg);
});
// 监听最初未捕获稍后又被捕获的Promise异常
window.addEventListener('rejectionhandled', function(e) {
var errorMesg = e.reason;
sendErrorToServer('最初未捕获稍后又被捕获的Promise异常:' + errorMesg);
});
// 已捕获的Promise异常
new Promise(function(resolve, reject) {
// reject('我被捕获了');
throw new Error('我被捕获了');
}).catch(function(reason) {
var errorMesg = reason;
sendErrorToServer('已捕获的Promise异常:' + errorMesg);
});
// 未捕获的Promise异常
new Promise(function(resolve, reject) {
reject('我没有被捕获');
});
// 最初未捕获稍后又被捕获的Promise异常:
var p1 = new Promise(function(resolve, reject) {
reject('我后来被捕获了');
});
setTimeout(function(){
p1.catch(function(e) {
});
}, 200);
function sendErrorToServer(error) {
const img = new Image();
var from = window.location.href;
error += '\n' + from;
img.src = "http://www.baidu.com?error=" + error;
}
function wrapError({message='', source='', lineno='', colno='', error, target}) {
var returned = '';
var mesg = error && error.toString() || message;
var stack = error && error.stack || '';
returned += '\n' + mesg;
returned += '\n' + source;
returned += '\n' + lineno;
returned += '\n' + colno;
returned += '\n' + stack;
if (target && target.outerHTML) {
returned += '\n' + target.outerHTML;
}
return returned;
}
复制代码
使用try/catch块的try语句将可能出错的代码包含进去。
在try/catch块的catch语句中存在非注释的代码逻辑能够认为错误通过处理。
与之相反,catch(e) {}内部没有代码逻辑,能够认为错误没有通过处理。
使用throw语句后跟错误对象,throw new Error('error message')
或 throw 'error message'
。
最后发个广告,网易七鱼招聘前端开发工程师,挑战高薪传送门