本系列其他译文请看[JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn)] (juejin.cn/column/6988…
本文阅读指数:5
异常处理在前端工程中是非常常见,也非常重要的。但是重视的人并不多。建议充分阅读。
概述
异常检测是保证程序能够可靠运行的一个技术手段。
异常检测的一个途径是异常检查。异常检查维持正常的程序流程,同时进行异常的检查,它使用特殊的返回值,辅助的全局变量或者浮点状态值来上报异常。
异常在程序执行的过程中发生,并且会中断正常的流程。这样的中断会触发预定义的异常处理函数。
注意,硬件和软件都可能发生异常。
JS中的异常
一个JS应用可以运行在各种操作系统,浏览器或者硬件设备中。无论你写多少测试,面对这么复杂的环境,总是会有异常的。 从终端用户的角度看,JS都是默默处理异常的。但是其背后的机制却有点复杂。 只要一部分代码出错,JS就会抛出一个异常。JS引擎就不会继续执行代码了,而是要检查一下是否有异常处理的句柄函数。
如果没有异常句柄,引擎就会return,然后抛出一个异常。然后调用栈中的每一个函数都会重复这个步骤,直到找到处理异常的句柄。如果到最后都没有找到句柄,栈中也没有函数了,那么event loop就会把回调队里中的下一个函数加入到栈中。
异常发生时,会生成一个Error对象,并抛出异常。
Error对象的类型
JS内置了9中异常对象,他们是异常处理的根本:
- Error - 表示一般通用的异常,经常用来实现用户自定义的异常。
- EvalError- 没有正确使用
eval()
函数时发生的 - RangeError - 访问数字变量或者参数时,超出了它的可能范围时发生
- ReferenceError - 访问一个不存在的变量时发生
- SyntaxError - 没有遵循JS语法规则时发生。对于静态语言,这个错误是在编译时触发。对于JS则是在执行时触发。
- TypeError — 当一个值跟预期类型不匹配时发生。调用一个不存在的对象方法,也会引起这个异常
- URIError — 调用
encodeURI()
和decodeURI()
时遇到了不合法的URL - AggregateError — 多个异常需要被合并到一次上报中时发生,比如
Promise.any()
- InternalError — JS引擎内部抛出的异常。比如“递归太多”,这个API目前还不是标准化的。
通过继承内置异常,你还可以自定义异常类型。
抛出异常
JS允许开发者调用throw
来触发异常。
if (denominator === 0) {
throw new RangeError("Attempted division by zero");
}
每一个内置的异常对象,具有一个可选的'message'参数,这样可以异常描述可读性更好。
你可以抛出任何类型的异常,比如数字,字符串,数组等等
throw true;
throw 113;
throw ‘error message’;
throw null;
throw undefined;
throw {x: 1};
throw new SyntaxError(‘hard to debug’);
这些都是有效的JS 声明。
使用内置的异常类型比其他对象摇号,因为浏览器会特殊照顾他们,比如引起异常的文件名,行数,调用栈跟踪等。一些浏览器,比如firefox,会为所有类型的异常对象收集这些属性
处理异常
现在看看如何保证异常不会摧毁我们的应用吧。
“try” 语句
跟其他编程语言类似,JS 具有try
, catch
, finally
声明,让我们得以控制异常流。
比如:
try {
// a function that potentially throws an error
someFunction();
} catch (err) {
// this code handles exceptions
console.log(e.message);
} finally {
// this code will always be executed
console.log(finally’);
}
try
语句强制性的包裹住可能抛出异常的代码块。
“catch” 语句
“catch” 语句紧随其后,它包住了异常处理的代码块。“catch” 语句让异常不在扩散,允许程序继续执行。 异常本身作为一个参数被传递到catch语句。
一些代码块可以抛出不同类型的异常,你的应用可以支持多种异常。
instanceof
操作可以用来区分不同类型的异常
try {
If (typeof x !== ‘number’) {
throw new TypeError(‘x is not a number’);
} else if (x <= 0) {
throw new RangeError(‘x should be greater than 0’);
} else {
// Do something useful
}
} catch (err) {
if (err instanceof TypeError) {
// Handle TypeError exceptions
} else if (err instanceof RangeError) {
// Handle RangeError exceptions
} else {
// Handle all other types of exceptions
}
}
这个例子可以重抛一个捕获的异常。比如你虽然捕获了异常,但是它跟你的上下文没有关系,那就可以再次抛出去。、
“finally” 语句
finally
代码块在try
和 catch
之后执行,无视任何异常(话句话说,只要发生了异常,那么finally就一定会执行)。finally
语句可以用来执行一些清理工作,比如关闭WebSocket连接等。
即使异常没有被捕获,finally
块也会执行。在这种场景下,finally块执行之后,引擎会继续按顺序检查调用栈中的函数,直到找到正确的异常句柄或者直到应用被关闭。
同样,即使try
或者catch
已经执行了了return
,finally也还是会执行。
看一个例子:
function foo1() {
try {
return true;
} finally {
return false;
}
}
执行 foo1()
函数,得到返回值false
,即使try
已经有一个return
声明了。
下面的例子是同样的结果:
function foo2() {
try {
throw new Error();
} catch {
return true;
} finally {
return false;
}
}
执行 foo1()
函数,得到返回值false
处理异步代码中的异常
之前讨论过JS中异步编程的机制,这里我们看看如果处理“callback functions”, “promises”, 和 “async/await”中的异常。
async/await
定义一个标准的函数,抛出一个异常
async function foo() {
throw new Error();
}
当异常在async
函数中抛出时,返回的是一个‘rejected’的promise,并伴随了抛出的异常
return Promise.Reject(new Error())
看看当调用foo()
时发生了什么
try {
foo();
} catch(err) {
// This block won’t be reached.
} finally {
// This block will be reached before the Promise is rejected.
}
由于foo()
是异步,它分发了一个 Promise
。代码不会等待async
函数结束,所以此时其实没有真正的捕捉到异常。finally
块会执行,然后返回一个Promise
并且rejected。
我们没有任何代码来处理这个被rejected的Promise
。
在调用foo()
时可以添加一个await
关键字,并且用async
函数包含这段代码,就可以处理这个promise了。
async function run() {
try {
await foo();
} catch(err) {
// This block will be reached now.
} finally {
// This block will be reached at the end.
}
}
run();
Promises
顶一个函数,在Promise
外面扔出一个异常
function foo(x) {
if (typeof x !== 'number') {
throw new TypeError('x is not a number');
}
return new Promise((resolve, reject) => {
resolve(x);
});
}
现在给 foo
传递一个 string
而不是number
foo(‘test’)
.then(x => console.log(x))
.catch(err => console.log(err));
这会引起Uncaught TypeError: x is not a number
,因为promise的catch
还不能处理异常--它是在Promise
之外抛出的
使用标准的try
和 catch
语句可以捕获这个异常
try {
foo(‘test’)
.then(x => console.log(x))
.catch(err => console.log(err));
} catch(err) {
// Now the error is handed
}
如果修改foo
,在Promise
内部抛出异常
function foo(x) {
return new Promise((resolve, reject) => {
if (typeof x !== 'number') {
throw new TypeError('x is not a number');
}
resolve(x);
});
}
现在catch
声明就会处理这个异常
try {
foo(‘test’)
.then(x => console.log(x))
.catch(err => console.log(err)); // The error is handled here.
} catch(err) {
// This block is not reached since the thrown error is inside of a Promise.
}
注意,在Promise
中抛出异常和使用reject
回调是一样的。所以可以这样定义foo
function foo(x) {
return new Promise((resolve, reject) => {
if (typeof x !== 'number') {
reject('x is not a number');
}
resolve(x);
});
}
如果没有catch
方法来处理promise
内部的异常,回调队列中的下一个函数就会被添加到调用栈上。
Callback Functions
使用异常优先的回调策略有两个主要原则:
- 回调的第一个参数是error对象,如果发生了异常,它的第一个参数会被设置为
err
然后返回。如果没有异常,err
为null - 回调的第二个参数是响应过来的数据
function asyncFoo(x, callback) {
// Some async code...
}
asyncFoo(‘testParam’, (err, result) => {
If (err) {
// Handle error.
}
// Do some other work.
});
如果有一个 err
对象,最好不要触碰或者依赖result
参数。
未处理的异常怎么办
如果使用了第三方的库,你就没有权限去处理异常了。当你想处理一些没有句柄的异常时,可以看看下面的例子
浏览器
浏览器中的window.onerror
事件可以处理这种情况:
例子:
window.onerror = (msg, url, line, column, err) => {
// ... handle error …
return false;
};
它的参数是这样的:
- msg — 异常关联的信息,比如
Uncaught ReferenceError: foo is not defined
- url — 跟这个异常有关的脚本或者文档的URL
- lineNo — 代码行数(如果有)
- columnNo — 代码列数(如果有)
- err — 异常对象(如果有).
如果一个函数返回true,会阻止默认事件句柄的触发。
每次只能给window.onerror
赋值一个事件句柄。
这意味如果你想赋值,那么就要覆盖之前的被第三方库已经写好的句柄。这可能会引起大问题,尤其是像一些异常跟踪的工具,它们可能完全停摆。
使用下面的小技巧,可以解决这个问题
var oldOnErrorHandler = window.onerror;
window.onerror = (msg, url, line, column, err) => {
If (oldOnErrorHandler) {
// Call any previously assigned handler.
oldOnErrorHandler.apply(this, arguments);
}
// The rest of your code
}
这个代码首先检查了是否之前已经定义好了window.onerror
,然后在处理前简单的调用一下。使用这种办法,就可以随心所欲的在window.onerror
上加句柄了
这种方式在各个浏览器中都能实现。
另外一个不需要替代句柄的方式,是给window
对象加事件监听
window.addEventListener('error', e => {
// Get the error properties from the error event object
const { message, filename, lineno, colno, error } = e;
});
这种方式更好,支持更广泛
Node.js
EventEmmiter
模块中的process
对象,提供两个事件来处理异常:
uncaughtException
-- 当一个未捕获的异常冒泡到了[event loop]时会发生。Node.js默认会把这异常的栈跟踪打印到stderr,然后退出并返回code 1
。可以为这这个事件添加一个句柄。使用这个事件的恰当的方式是在线程关闭之前,执行异步的资源清理(比如文件描述符,句柄等等)。这之后再执行正常操作是不安全的。unhandledRejection
—当Promise
被rejected,并且没有异常处理时触发。在探查和跟踪被rejected并且没有异常句柄的promise时,unhandledRejection
很有用。
process
.on('unhandledRejection', (reason, promise) => {
// Handle failed Promise
})
.on('uncaughtException', err => {
// Handle failed Error
process.exit(1);
});
在代码中正确的处理异常是非常重要的,只有理解未处理异常,你才能够正确的处理它们。
你可以自己做,但是可能会有点麻烦,你需要考虑各种浏览器的不同场景。你也可以使用一些第三方的的工具来实现。不管怎么做,你都需要尽可能多的理解异常和异常触发的上下文,这样才能轻松的复现异常。