JavaScript错误处理终极指南
本教程将深入探讨JavaScript错误处理,以便您能够抛出、检测和处理自己的错误。
不存在没有bug的系统,如果有什么问题出现,最可能的情况是第一个用户第一次访问的时候。我们可以通过以下方法避免一些Web系统的bug:
- 一个好用的编辑器和Lint校验规则。
- 好的验证和用户错误捕捉。
- 好的测试流程。
然而,错误仍然存在。浏览器可能会失败或不支持我们使用的API。服务器可能会崩溃或响应时间过长。网络连接可能会失败或变得不可靠。问题可能是暂时的,但我们无法通过编码来解决此类问题。但是,我们可以预见问题,采取补救措施,并使我们的应用程序更具弹性。
显示错误消息是最后的手段
理想情况下,用户永远不应该看到错误消息。 我们可能可以忽略较小的问题,例如一些图标无法加载。我们可以通过将数据存储在本地并稍后上传来解决更严重的问题,例如Ajax数据保存失败。只有当用户有可能丢失数据时,才需要出现错误——前提是他们可以采取某些措施。 因此,需要在发生错误时捕获错误并确定最佳操作。在JavaScript应用程序中引发和捕获错误可能一开始会让人很难,但它可能比您想象的要容易。
JavaScript如何处理错误
当JavaScript语句导致错误时,它被称为引发异常。JavaScript创建并引发描述错误的Error对象。下面的函数会在dp
为负数时引发错误:
// division calculation
function divide(v1, v2, dp) {
return (v1 / v2).toFixed(dp);
}
在抛出错误之后,JavaScript解释器会检查异常处理代码。divide()
函数中没有异常处理程序,因此它会检查调用该函数的函数:
// show result of division
function showResult() {
result.value = divide(
parseFloat(num1.value),
parseFloat(num2.value),
parseFloat(dp.value)
);
}
解释器会针对调用栈上的每个函数重复此过程,直到发生以下情况之一:
- 它找到一个异常处理程序。
- 它达到代码的顶层。
捕获异常
我们可以使用try...catch
块在divide()
函数中添加异常处理程序:
// division calculation
function divide(v1, v2, dp) {
try {
return (v1 / v2).toFixed(dp);
}
catch(e) {
console.log(`
error name : ${ e.name }
error message: ${ e.message }
`);
return 'ERROR';
}
}
这将执行try{}
块中的代码,但是当发生异常时,会执行catch{}
块并接收抛出的错误对象。注意:对于像divide()
这样的基本函数,try...catch
块的演示有些过分。更简单的方法是确保dp
为零或更高,后面我们会看到。 如果我们需要在try
或catch
代码执行时运行代码,可以定义一个可选的finally{}
块:
function divide(v1, v2, dp) {
try {
return (v1 / v2).toFixed(dp);
}
catch(e) {
return 'ERROR';
}
finally {
console.log('done');
}
}
控制台输出“done”,无论计算成功还是引发错误。finally
块通常执行我们需要在try
块和catch
块中重复执行的操作,例如取消API调用或关闭数据库连接。 try
块要求有catch
块、finally
块或两者都有。请注意,当finally
块包含return
语句时,该值将成为整个函数的返回值;在try
或catch
块中的其他return
语句将被忽略。
嵌套异常处理
如果我们在调用showResult()
函数中添加一个异常处理程序会发生什么?
// show result of division
function showResult() {
try {
result.value = divide(
parseFloat(num1.value),
parseFloat(num2.value),
parseFloat(dp.value)
);
}
catch(e) {
result.value = 'FAIL!';
}
}
答案是……*什么都没有!*这个catch
块永远不会被执行,因为divide()
函数中的catch
块处理了错误。 不过,我们可以在divide()
函数中编程地throw一个新的Error
对象,并在第二个参数的cause
属性中可选地传递原始错误:
function divide(v1, v2, dp) {
try {
return (v1 / v2).toFixed(dp);
}
catch(e) {
throw new Error('ERROR', { cause: e });
}
}
这将触发调用函数中的catch
:
// show result of division
function showResult() {
try {
//...
}
catch(e) {
console.log( e.message ); // ERROR
console.log( e.cause.name ); // RangeError
result.value = 'FAIL!';
}
}
JavaScript标准错误类型
当发生异常时,JavaScript会创建并抛出一个对象,描述使用以下类型之一的错误。
SyntaxError
由语法无效的代码引发的错误,例如缺失的括号:
if condition) { // SyntaxError
console.log('condition is true');
}
注意:像C++和Java这样的语言在编译期间报告语法错误。JavaScript是一种解释性语言,因此在代码运行之前不会识别语法错误。任何优秀的代码编辑器或Lint程序都可以在尝试运行代码之前发现语法错误。
ReferenceError
当访问不存在的变量时引发的错误:
function inc() {
value++; // ReferenceError
}
TypeError
当值不是预期类型时引发的错误,例如调用不存在的对象方法:
const obj = {};
obj.missingMethod(); // TypeError
RangeError
当值不在允许的值集合或范围内时引发的错误。上面使用的toFixed()
方法会生成此错误,因为它通常期望一个值介于0
和100
之间:
const n = 123.456;
console.log( n.toFixed(-1) ); // RangeError
URIError
当URI处理函数(例如encodeURI()
和decodeURI()
)遇到格式不正确的URI时引发的错误:
const u = decodeURIComponent('%'); // URIError
EvalError
当将包含无效JavaScript代码的字符串传递给eval()
函数时引发的错误:
eval('console.logg x;'); // EvalError
注意:请不要使用eval()
!执行可能包含来自用户输入的字符串中的任意代码太危险了!
AggregateError
当多个错误包装在单个错误中时引发的错误。通常在调用操作(如Promise.all()
)时引发,该操作返回来自任意数量的Promise的结果。
InternalError
当JavaScript引擎内部发生错误时引发的非标准(仅限Firefox)错误。通常这是某些东西占用了太多的内存,例如一个大的数组或“过多的递归”。
Error
最后,有一个通用的Error
对象,通常在实现我们自己的异常时使用……下一节我们将讨论此内容。
自定义错误
当发生错误或应该发生错误时,我们可以抛出自定义异常。例如:
- 我们的函数未传递有效参数
- Ajax请求未返回预期数据
- DOM更新失败,因为该节点不存在
throw
语句实际上接受任何值或对象。例如:
throw 'A simple error string';
throw 42;
throw true;
throw { message: 'An error', name: 'MyError' };
异常会被抛到调用堆栈上的每个函数,直到它们被异常(catch)处理程序拦截。然而,更实际地,我们将想要创建并抛出一个Error
对象,以便它们与JavaScript抛出的标准错误相同。
我们可以通过向构造函数传递可选消息来创建通用的Error
对象:
throw new Error('An error has occurred');
我们还可以像使用函数一样使用Error
,而不是使用new
。它返回与上面相同的Error
对象:
throw Error('An error has occurred');
我们可以可选地将文件名和行号作为第二个和第三个参数传递:
throw new Error('An error has occurred', 'script.js', 99);
这很少是必要的,因为它们默认为我们抛出Error
对象的文件和行号。 (它们在文件更改时也难以维护!)
我们可以定义通用的Error
对象,但我们应该在可能的情况下使用标准Error
类型。例如:
throw new RangeError('Decimal places must be 0 or greater');
所有Error
对象都具有以下属性,我们可以在catch
块中检查这些属性:
.name
: 错误类型的名称-如Error
或RangeError
.message
: 错误消息 以下非标准属性在Firefox中也受支持:.fileName
: 发生错误的文件.lineNumber
: 错误发生的行号.columnNumber
: 错误发生行的列号.stack
: 列出错误发生之前进行的函数调用的堆栈跟踪
我们可以将divide()
函数更改为在小数位数不是数字,小于零或大于八时抛出RangeError
:
// division calculation
function divide(v1, v2, dp) {
if (isNaN(dp) || dp < 0 || dp > 8) {
throw new RangeError('Decimal places must be between 0 and 8');
}
return (v1 / v2).toFixed(dp);
}
类似地,我们可以在被除数值不是数字时抛出Error
或TypeError
,以防止得到NaN
的结果:
if (isNaN(v1)) {
throw new TypeError('Dividend must be a number');
}
我们还可以为非数字或零的除数进行处理。JavaScript在除以零时返回Infinity
,但这可能会困惑用户。我们可以不使用通用的Error
,而是创建一个自定义的DivByZeroError
错误类型:
// new DivByZeroError Error type
class DivByZeroError extends Error {
constructor(message) {
super(message);
this.name = 'DivByZeroError';
}
}
然后以同样的方式抛出它:
if (isNaN(v2) || !v2) {
throw new DivByZeroError('Divisor must be a non-zero number');
}
现在在调用的showResult()
函数中添加一个try ... catch
块。它可以接收任何Error
类型并相应地做出反应-在这种情况下,显示错误消息:
// show result of division
function showResult() {
try {
result.value = divide(
parseFloat(num1.value),
parseFloat(num2.value),
parseFloat(dp.value)
);
errmsg.textContent = '';
}
catch (e) {
result.value = 'ERROR';
errmsg.textContent = e.message;
console.log( e.name );
}
}
在JavaScript中,抛出和处理异常是一种重要的编程技巧。JavaScript的异常处理与其他编程语言的异常处理非常相似,主要使用try...catch
语句块来捕获错误和异常。
同步函数错误
在同步函数中,我们可以使用throw
语句抛出一个Error
对象,然后使用try...catch
语句块来处理错误。例如:
// division calculation
function divide(v1, v2, dp) {
if (isNaN(v1)) {
throw new TypeError('Dividend must be a number');
}
if (isNaN(v2) || !v2) {
throw new DivByZeroError('Divisor must be a non-zero number');
}
if (isNaN(dp) || dp < 0 || dp > 8) {
throw new RangeError('Decimal places must be between 0 and 8');
}
return (v1 / v2).toFixed(dp);
}
不再需要在最终的return
语句周围放置try...catch
块,因为它不应该生成错误。如果出现错误,JavaScript会生成自己的错误,并由showResult()
函数中的catch
块来处理。
异步函数错误
对于基于回调的异步函数,我们不能使用try...catch
语句块来捕获异常,因为错误发生在try...catch
块执行完成后。以下代码看起来正确,但catch
块永远不会执行,控制台会在一秒钟后显示一个Uncaught Error
消息:
function asyncError(delay = 1000) {
setTimeout(() => {
throw new Error('I am never caught!');
}, delay);
}
try {
asyncError();
}
catch(e) {
console.error('This will never run');
}
大多数框架和服务器运行时(如Node.js)中的约定假定将错误作为回调函数的第一个参数返回。这不会引发异常,尽管我们可以手动抛出一个Error
对象:
function asyncError(delay = 1000, callback) {
setTimeout(() => {
callback('This is an error message');
}, delay);
}
asyncError(1000, e => {
if (e) {
throw new Error(`error: ${ e }`);
}
});
基于Promise的错误
当使用Promise
作为异步函数的返回值时,我们可以使用reject()
方法返回一个新的Error
对象或任何其他值来表示错误:
function wait(delay = 1000) {
return new Promise((resolve, reject) => {
if (isNaN(delay) || delay < 0) {
reject( new TypeError('Invalid delay') );
}
else {
setTimeout(() => {
resolve(`waited ${ delay } ms`);
}, delay);
}
})
}
当传递无效的delay
参数时,Promise.catch()
方法会执行,并接收到返回的Error
对象:
// invalid delay value passed
wait('INVALID')
.then( res => console.log( res ))
.catch( e => console.error( e.message ) )
.finally( () => console.log('complete') );
我们还可以使用await
关键字调用返回Promise
的任何函数。这必须在async
函数内部进行,但我们可以使用标准的try...catch
语句块来捕获错误:
(async () => {
try {
console.log( await wait('INVALID') );
}
catch (e) {
console.error( e.message );
}
finally {
console.log('complete');
}
})();
异常处理
抛出Error
对象并处理异常在JavaScript中非常容易:
try {
throw new Error('I am an error!');
}
catch (e) {
console.log(`error ${ e.message }`)
}
如果您想更深入地学习JavaScript中的错误处理,请参考以下资源:
转载自:https://juejin.cn/post/7256590619412709431