JavaScript错误处理终极指南,本教程深入探讨 JavaScript 错误处理,以便您能够抛出、检测和处理您自己的错误。
专家开发人员期待意外。如果某事可能出错,它就会出错——通常是在第一个用户访问您的新网络系统的那一刻。
我们可以避免一些 web 应用程序错误,如下所示:
- 一个好的编辑器或 linter 可以捕获语法错误。
- 良好的验证可以捕获用户输入错误。
- 强大的测试过程可以发现逻辑错误。
然而错误依然存在。浏览器可能会失败或不支持我们正在使用的 API。服务器可能会失败或响应时间过长。网络连接可能会失败或变得不可靠。问题可能是暂时的,但我们无法通过编码解决此类问题。但是,我们可以预测问题,采取补救措施,并使我们的应用程序更具弹性。
显示错误消息是最后的手段
理想情况下,用户永远不会看到错误消息。
我们或许可以忽略一些小问题,例如装饰图片加载失败。我们可以通过在本地存储数据并稍后上传来解决更严重的问题,例如 Ajax 数据保存失败。只有当用户面临丢失数据的风险时,错误才变得必要——假设他们可以对此做些什么。
因此,有必要在错误发生时捕获错误并确定最佳操作。在 JavaScript 应用程序中引发和捕获错误起初可能令人生畏,但它可能比您预期的要容易。
JavaScript 如何处理错误
当 JavaScript 语句导致错误时,我们称之为抛出异常。JavaScript 创建并抛出一个Error
描述错误的对象。我们可以在这个 CodePen 演示中看到这一点。如果我们将小数位数设置为负数,我们将在底部的控制台中看到一条错误消息。(请注意,我们没有在本教程中嵌入 CodePens,因为您需要能够看到控制台输出才能使它们有意义。)
结果不会更新,我们会RangeError
在控制台中看到一条消息。以下函数在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) ); }
解释器对调用堆栈上的每个函数重复该过程,直到发生以下情况之一:
- 它找到一个异常处理程序
- 它到达代码的顶层(这会导致程序终止并在控制台中显示错误,如上面的 CodePen 示例所示)
捕获异常
我们可以使用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 {}
块将执行并接收抛出的错误对象。和以前一样,尝试在此 CodePen 演示中将小数位设置为负数。
结果现在显示ERROR。控制台显示错误名称和消息,但这是由console.log
语句输出的,不会终止程序。
注意:这个try...catch
块的演示对于基本功能(如divide()
. 确保dp
为零或更高更简单,我们将在下面看到。
finally {}
如果我们需要在try
或代码执行时运行代码,我们可以定义一个可选块catch
:
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
语句时,该值将成为整个函数的返回值;或块中的其他return
语句将被忽略。try
catch
嵌套的异常处理程序
如果我们向调用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
永远不会到达此块,因为函数catch
中的块divide()
处理错误。
但是,我们可以通过编程方式抛出一个新Error
对象,并有选择地在第二个参数divide()
的属性中传递原始错误: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 使用以下类型之一创建并抛出描述错误的对象。
语法错误
语法无效代码引发的错误,例如缺少括号:
if condition) { // SyntaxError console.log('condition is true'); }
注意:C++、Java等语言编译时报语法错误。JavaScript 是一种解释型语言,因此在代码运行之前不会发现语法错误。任何好的代码编辑器或 linter 都可以在我们尝试运行代码之前发现语法错误。
参考错误
访问不存在的变量时抛出的错误:
function inc() { value++; // ReferenceError }
同样,优秀的代码编辑器和 linter 可以发现这些问题。
类型错误
当值不是预期类型时抛出错误,例如调用不存在的对象方法:
const obj = {}; obj.missingMethod(); // TypeError
范围误差
当值不在允许值的集合或范围内时抛出错误。上面使用的toFixed() 方法会产生此错误,因为它需要一个通常介于 0 和 100 之间的值:
const n = 123.456; console.log( n.toFixed(-1) ); // RangeError
URI错误
URI 处理函数(例如encodeURI()和decodeURI())在遇到格式错误的 URI 时抛出的错误:
const u = decodeURIComponent('%'); // URIError
评估错误
将包含无效 JavaScript 代码的字符串传递给eval() 函数时抛出错误:
eval('console.logg x;'); // EvalError
注意:请不要使用eval()
!执行可能由用户输入构造的字符串中包含的任意代码太危险了!
聚合错误
当多个错误包含在一个错误中时抛出的错误。这通常在调用诸如Promise.all() 之类的操作时引发,该操作返回任意数量的承诺的结果。
内部错误
JavaScript 引擎内部发生错误时抛出的非标准(仅限 Firefox)错误。这通常是某些东西占用过多内存的结果,例如大数组或“太多递归”。
错误
最后,还有一个通用Error
对象,它在实现我们自己的异常时最常使用……我们将在接下来介绍。
抛出我们自己的异常
我们可以throw
在错误发生时(或应该发生错误)创建自己的异常。例如:
- 我们的函数没有传递有效参数
- 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
不使用 like 函数new
。它返回一个Error
与上面相同的对象:
throw Error('An error has occurred');
我们可以选择将文件名和行号作为第二个和第三个参数传递:
throw new Error('An error has occurred', 'script.js', 99);
这很少是必要的,因为它们默认为我们放置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
or来防止结果: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'); }
现在try...catch
向调用showResult()
函数添加一个块。它可以接收任何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 ); } }
尝试在此 CodePen 演示中输入无效的非数字、零和负值。
该divide()
函数的最终版本检查所有输入值并在必要时抛出适当的异常Error
:
// 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); }
不再需要try...catch
在 final 周围放置一个块return
,因为它永远不会产生错误。如果确实发生了,JavaScript 将生成自己的错误并由catch
.showResult()
异步函数错误
我们无法捕获基于回调的异步函数抛出的异常,因为在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 }`); } });
基于承诺的错误
回调可能变得笨拙,因此在编写异步代码时最好使用promises 。当发生错误时,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); } }) }
注意:函数必须是 100% 同步或 100% 异步。这就是为什么有必要检查delay
返回的承诺中的值的原因。如果我们在返回 promise之前delay
检查值并抛出错误,则该函数将在发生错误时变为同步。
Promise.catch () 方法在传递无效delay
参数时执行,并接收到返回的Error
对象:
// invalid delay value passed wait('INVALID') .then( res => console.log( res )) .catch( e => console.error( e.message ) ) .finally( () => console.log('complete') );
就个人而言,我发现 promise 链有点难以阅读。幸运的是,我们可以使用它await
来调用任何返回承诺的函数。这必须发生在async
函数内部,但我们可以使用标准try...catch
块捕获错误。
以下(立即调用的)async
函数在功能上与上面的承诺链相同:
(async () => { try { console.log( await wait('INVALID') ); } catch (e) { console.error( e.message ); } finally { console.log('complete'); } })();
异常异常处理
在 JavaScript 中抛出Error
对象和处理异常很容易:
try { throw new Error('I am an error!'); } catch (e) { console.log(`error ${ e.message }`) }
构建一个对错误做出适当反应并让用户的生活更轻松的弹性应用程序更具挑战性。总是期待意想不到的事情。