本教程将深入探讨JavaScript错误处理,以便您能够抛出、检测和处理自己的错误。
不存在没有bug的系统,如果有什么问题出现,最可能的情况是第一个用户第一次访问的时候。我们可以通过以下方法避免一些Web系统的bug:
- 一个好用的编辑器和Lint校验规则。
- 好的验证和用户错误捕捉。
- 好的测试流程。
然而,错误仍然存在。浏览器可能会失败或不支持我们使用的API。服务器可能会崩溃或响应时间过长。网络连接可能会失败或变得不可靠。问题可能是暂时的,但我们无法通过编码来解决此类问题。但是,我们可以预见问题,采取补救措施,并使我们的应用程序更具弹性。
显示错误消息是最后的手段
理想情况下,用户永远不应该看到错误消息。 我们可能可以忽略较小的问题,例如一些图标无法加载。我们可以通过将数据存储在本地并稍后上传来解决更严重的问题,例如Ajax数据保存失败。只有当用户有可能丢失数据时,才需要出现错误——前提是他们可以采取某些措施。 因此,需要在发生错误时捕获错误并确定最佳操作。在JavaScript应用程序中引发和捕获错误可能一开始会让人很难,但它可能比您想象的要容易。
JavaScript如何处理错误
当JavaScript语句导致错误时,它被称为引发异常。JavaScript创建并引发描述错误的Error对象。下面的函数会在dp
为负数时引发错误:
// division calculationfunction divide(v1, v2, dp) {return (v1 / v2).toFixed(dp);}// division calculation function divide(v1, v2, dp) { return (v1 / v2).toFixed(dp); }// division calculation function divide(v1, v2, dp) { return (v1 / v2).toFixed(dp); }
在抛出错误之后,JavaScript解释器会检查异常处理代码。divide()
函数中没有异常处理程序,因此它会检查调用该函数的函数:
// show result of divisionfunction showResult() {result.value = divide(parseFloat(num1.value),parseFloat(num2.value),parseFloat(dp.value));}// show result of division function showResult() { result.value = divide( parseFloat(num1.value), parseFloat(num2.value), parseFloat(dp.value) ); }// show result of division function showResult() { result.value = divide( parseFloat(num1.value), parseFloat(num2.value), parseFloat(dp.value) ); }
解释器会针对调用栈上的每个函数重复此过程,直到发生以下情况之一:
- 它找到一个异常处理程序。
- 它达到代码的顶层。
捕获异常
我们可以使用try...catch
块在divide()
函数中添加异常处理程序:
// division calculationfunction divide(v1, v2, dp) {try {return (v1 / v2).toFixed(dp);}catch(e) {console.log(`error name : ${ e.name }error message: ${ e.message }`);return 'ERROR';}}// 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'; } }// 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');}}function divide(v1, v2, dp) { try { return (v1 / v2).toFixed(dp); } catch(e) { return 'ERROR'; } finally { console.log('done'); } }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 divisionfunction showResult() {try {result.value = divide(parseFloat(num1.value),parseFloat(num2.value),parseFloat(dp.value));}catch(e) {result.value = 'FAIL!';}}// show result of division function showResult() { try { result.value = divide( parseFloat(num1.value), parseFloat(num2.value), parseFloat(dp.value) ); } catch(e) { result.value = 'FAIL!'; } }// 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 });}}function divide(v1, v2, dp) { try { return (v1 / v2).toFixed(dp); } catch(e) { throw new Error('ERROR', { cause: e }); } }function divide(v1, v2, dp) { try { return (v1 / v2).toFixed(dp); } catch(e) { throw new Error('ERROR', { cause: e }); } }
这将触发调用函数中的catch
:
// show result of divisionfunction showResult() {try {//...}catch(e) {console.log( e.message ); // ERRORconsole.log( e.cause.name ); // RangeErrorresult.value = 'FAIL!';}}// show result of division function showResult() { try { //... } catch(e) { console.log( e.message ); // ERROR console.log( e.cause.name ); // RangeError result.value = 'FAIL!'; } }// 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) { // SyntaxErrorconsole.log('condition is true');}if condition) { // SyntaxError console.log('condition is true'); }if condition) { // SyntaxError console.log('condition is true'); }
注意:像C++和Java这样的语言在编译期间报告语法错误。JavaScript是一种解释性语言,因此在代码运行之前不会识别语法错误。任何优秀的代码编辑器或Lint程序都可以在尝试运行代码之前发现语法错误。
ReferenceError
当访问不存在的变量时引发的错误:
function inc() {value++; // ReferenceError}function inc() { value++; // ReferenceError }function inc() { value++; // ReferenceError }
TypeError
当值不是预期类型时引发的错误,例如调用不存在的对象方法:
const obj = {};obj.missingMethod(); // TypeErrorconst obj = {}; obj.missingMethod(); // TypeErrorconst obj = {}; obj.missingMethod(); // TypeError
RangeError
当值不在允许的值集合或范围内时引发的错误。上面使用的toFixed()
方法会生成此错误,因为它通常期望一个值介于0
和100
之间:
const n = 123.456;console.log( n.toFixed(-1) ); // RangeErrorconst n = 123.456; console.log( n.toFixed(-1) ); // RangeErrorconst n = 123.456; console.log( n.toFixed(-1) ); // RangeError
URIError
当URI处理函数(例如encodeURI()
和decodeURI()
)遇到格式不正确的URI时引发的错误:
const u = decodeURIComponent('%'); // URIErrorconst u = decodeURIComponent('%'); // URIErrorconst u = decodeURIComponent('%'); // URIError
EvalError
当将包含无效JavaScript代码的字符串传递给eval()
函数时引发的错误:
eval('console.logg x;'); // EvalErroreval('console.logg x;'); // EvalErroreval('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' };throw 'A simple error string'; throw 42; throw true; throw { message: 'An error', name: 'MyError' };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');throw new Error('An error has occurred');throw new Error('An error has occurred');
我们还可以像使用函数一样使用Error
,而不是使用new
。它返回与上面相同的Error
对象:
throw Error('An error has occurred');throw Error('An error has occurred');throw Error('An error has occurred');
我们可以可选地将文件名和行号作为第二个和第三个参数传递:
throw new Error('An error has occurred', 'script.js', 99);throw new Error('An error has occurred', 'script.js', 99);throw new Error('An error has occurred', 'script.js', 99);
这很少是必要的,因为它们默认为我们抛出Error
对象的文件和行号。 (它们在文件更改时也难以维护!)
我们可以定义通用的Error
对象,但我们应该在可能的情况下使用标准Error
类型。例如:
throw new RangeError('Decimal places must be 0 or greater');throw new RangeError('Decimal places must be 0 or greater');throw new RangeError('Decimal places must be 0 or greater');
所有Error
对象都具有以下属性,我们可以在catch
块中检查这些属性:
.name
: 错误类型的名称-如Error
或RangeError
.message
: 错误消息 以下非标准属性在Firefox中也受支持:.fileName
: 发生错误的文件.lineNumber
: 错误发生的行号.columnNumber
: 错误发生行的列号.stack
: 列出错误发生之前进行的函数调用的堆栈跟踪
我们可以将divide()
函数更改为在小数位数不是数字,小于零或大于八时抛出RangeError
:
// division calculationfunction 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);}// 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); }// 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');}if (isNaN(v1)) { throw new TypeError('Dividend must be a number'); }if (isNaN(v1)) { throw new TypeError('Dividend must be a number'); }
我们还可以为非数字或零的除数进行处理。JavaScript在除以零时返回Infinity
,但这可能会困惑用户。我们可以不使用通用的Error
,而是创建一个自定义的DivByZeroError
错误类型:
// new DivByZeroError Error typeclass DivByZeroError extends Error {constructor(message) {super(message);this.name = 'DivByZeroError';}}// new DivByZeroError Error type class DivByZeroError extends Error { constructor(message) { super(message); this.name = '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');}if (isNaN(v2) || !v2) { throw new DivByZeroError('Divisor must be a non-zero number'); }if (isNaN(v2) || !v2) { throw new DivByZeroError('Divisor must be a non-zero number'); }
现在在调用的showResult()
函数中添加一个try ... catch
块。它可以接收任何Error
类型并相应地做出反应-在这种情况下,显示错误消息:
// show result of divisionfunction 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 );}}// 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 ); } }// 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 calculationfunction 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);}// 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); }// 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');}function asyncError(delay = 1000) { setTimeout(() => { throw new Error('I am never caught!'); }, delay); } try { asyncError(); } catch(e) { console.error('This will never run'); }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 }`);}});function asyncError(delay = 1000, callback) { setTimeout(() => { callback('This is an error message'); }, delay); } asyncError(1000, e => { if (e) { throw new Error(`error: ${ e }`); } });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);}})}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); } }) }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 passedwait('INVALID').then( res => console.log( res )).catch( e => console.error( e.message ) ).finally( () => console.log('complete') );// invalid delay value passed wait('INVALID') .then( res => console.log( res )) .catch( e => console.error( e.message ) ) .finally( () => console.log('complete') );// 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');}})();(async () => { try { console.log( await wait('INVALID') ); } catch (e) { console.error( e.message ); } finally { console.log('complete'); } })();(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 }`)}try { throw new Error('I am an error!'); } catch (e) { console.log(`error ${ e.message }`) }try { throw new Error('I am an error!'); } catch (e) { console.log(`error ${ e.message }`) }
如果您想更深入地学习JavaScript中的错误处理,请参考以下资源: