在任何软件开发项目中,处理错误和异常对应用程序的成功至关重要。无论 你是新手还是专家,你的代码都可能因为各种原因而失败,比如简单的拼写错误或来自外部服务的意外行为。
因为你的代码总是有失败的可能,所以你需要通过使你的代码更加健壮来准备处理这种情况。你可以通过多种方式做到这一点,但一种常见的解决方案是利用 try-catch
语句。这个语句允许你包装一个代码块来尝试和执行。如果在执行过程中发生了任何错误,该语句将“捕获”它们,你可以快速修复它们,避免应用程序崩溃。
本指南将作为 try-catch
语句的实用介绍,并向你展示如何使用它们处理 JavaScript 中的错误。
在学习 try-catch
语句之前,你需要了解 JavaScript 中的整体错误处理。在处理 JavaScript 中的错误时,可能会出现几种类型的错误。在本文中,你将重点关注两种类型:语法错误和运行时错误。在 深入了解 JavaScript 语法错误以及如何防止它们 中你可以了解更多关于语法错误的信息。
当你不遵循特定编程语言的规则时,就会发生语法错误。可以通过配置 linter 来检测这些错误,这是一种用来分析代码并标记风格错误或编程错误的工具。例如,你可以使用 ESLint 来查找和修复代码中的问题。
下面是一个语法错误的例子:
console.log(;)
在这种情况下,发生错误是因为代码的语法不正确。它应该如下:
console.log('Some Message');
当应用程序在运行时出现问题时,就会发生运行时错误。例如,你的代码可能试图调用一个不存在的方法。要捕获这些错误并应用一些处理,可以使用 try-catch
语句。
异常是表示在程序执行时发生了问题的对象。当访问无效数组索引时,当试图访问空引用的某些成员时,当试图调用不存在的函数时,都可能发生这些问题。
例如,考虑一个应用程序依赖第三方 API 的场景。你的代码可能会处理来自这个 API 的响应,期望它们包含某些属性。如果这个 API 由于任何原因返回一个意外的响应,它可能会触发一个运行时错误。在这种情况下,你可以将受影响的逻辑包装在 try-catch
语句中,并向用户提供错误消息,甚至调用一些回退逻辑,而不是允许错误导致应用程序崩溃。
简单地说,一个 try-catch
语句包含两个代码块——一个以 try
关键字为前缀,另一个以 catch
关键字为前缀——以及一个用于在其中存储错误对象的变量名。如果 try
块内部的代码抛出错误,则该错误将被传递到 catch
块进行处理。如果它不抛出错误,则永远不会调用 catch
块。考虑下面的例子:
try {
nonExistentFunction()
} catch (error) {
console.log(error); // [ReferenceError: nonExistentFunction is not defined]
}
在本例中,当函数被调用时,运行时将发现没有这样的函数并抛出错误。多亏了围绕它的 try-catch
语句,这个错误不是致命的,可以按照你喜欢的方式进行处理。在本例中,它被传递到 console.log
,它告诉你错误是什么。
还有另一个可选语句称为 finally
语句,当它出现时,总是在 try
和 catch
之后执行,而不管是否执行了catch
。它通常用于包含释放在 try
句期间分配的资源的命令,这些资源在发生错误时可能没有机会优雅地清理。考虑下面的例子:
openFile();
try {
writeData();
} catch (error) {
console.log(error);
} finally {
closeFile();
}
在这个人为的例子中,假设在 try-catch-finally
语句之前打开了一个文件。如果在 try
块期间发生了错误,那么一定要关闭文件,以避免内存泄漏或死锁。在本例中,finally
语句确保无论 try-catch
如何执行,文件都将在继续之前关闭。
当然,用 try-catch
语句包装可能容易出错的代码只是容错难题的一部分。另一部分是知道在抛出错误时该如何处理。
有时将它们显示给用户可能是有意义的(通常以更容易读懂的格式),有时你可能想简单地记录它们以供将来参考。无论哪种方式,熟悉 Error
对象本身都有帮助,这样你就知道必须处理哪些数据。
每当 try
语句中抛出异常时,JavaScript 会创建一个 Error
对象并将其作为参数发送给 catch
语句。通常,这个对象有两个主要的实例属性:
name
:描述错误类型的字符串message
:更详细的错误描述try {
nonExistentFunction();
} catch (error) {
const {name, message} = error;
console.log({ name, message }) // { name: 'ReferenceError', message: 'nonExistentFunction is not defined' }
}
如前所述,name
属性的值引用发生的 错误类型。以下是一些比较常见的错误类型的非详尽列表:
ReferenceError
:当检测到对不存在的或无效的变量或函数的引用时抛出ReferenceError
。TypeError
:当一个值以与其类型不兼容的方式使用时,例如试图调用一个数字的字符串函数((1).split(',');
), TypeError
将被抛出。SyntaxError
:当在解释代码时出现语法错误时抛出 SyntaxError
;例如,当解析 JSON 时使用后面的逗号(JSON.parse('[1,2,3,4,]');
)。SyntaxError
:当 URI 处理中发生错误时抛出 URIError
;例如,在decodeURI()
或 encodeURI()
中传入无效的参数。RangeError
:当一个值不在允许值的集合或范围内时抛出 RangeError
;例如,数字数组中的字符串值。所有原生 JavaScript 错误都是通用 Error
对象的扩展。根据这个原则,你还可以创建自己的错误类型。
JavaScript 中另一个与错误和错误处理密切相关的关键字是 throw
。使用此关键字时,可以“抛出”用户定义的异常。当你这样做时,当前函数将停止执行,并且与 throw
关键字一起使用的任何值将被传递给调用堆栈中的第一个 catch
语句。如果没有 catch
语句来处理它,行为将类似于典型的未处理错误,程序将终止。
抛出自定义错误在更复杂的应用程序中很有用,因为它为你提供了对代码进行流控制的另一种途径。考虑这样一个场景,你需要验证一些用户输入。如果根据业务规则认为输入无效,则不希望继续处理请求。这是 throw
语句的完美用例。考虑下面的例子:
// 你可以通过扩展泛型类来定义自己的错误类型
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
const userInputIsValid = false;
try {
if (!userInputIsValid) {
// 手动触发自定义错误
throw new ValidationError('User input is not valid');
}
} catch (error) {
const { name, message } = error;
console.log({ name, message }); // { name: 'ValidationError', message: 'User input is not valid' }
}
这里定义了一个自定义错误类,可以扩展泛型错误类。这种技术可用于抛出与业务逻辑专门相关的错误,而不仅仅是 JavaScript 引擎使用的默认错误。
因为错误会沿着调用堆栈向上攀升,直到它们找到一个 try-catch
语句或应用程序终止,所以很容易将整个应用程序简单地封装在一个大型的 try-catch
语句或许多较小的 try-catch
语句中,这样你就可以享受应用程序在技术上不会再次崩溃的事实。然而这不是一个好主意。
当涉及到编程时,错误是一个事实,它们在应用程序的生命周期中扮演着不可忽视的重要角色。他们会告诉你哪里出了问题。因此,如果你想为用户提供良好的体验,就必须尊重并谨慎地使用 try-catch
语句。
一般应该在合理预期可能发生错误的地方使用 try-catch
语句。但是,一旦捕捉到错误,通常不会想要直接删除它。如前所述,当抛出错误时,就意味着发生了错误。你应该利用这个机会适当地处理错误,无论是在 UI 中向用户显示更好的错误消息,还是将错误发送到应用程序监视工具(如果有的话),以便进行聚合和稍后的分析。
通常,在 try-catch
语句中包装代码时,应该只包装在概念上与预期错误相关的代码。如果函数的大小相当小,这可能意味着要包装整个函数体。或者,你可能有一个更大的函数,其中主体中的所有代码都被包装了,但分散在多个 try-catch
语句中。这实际上可以作为一种指示,表明有问题的功能过于复杂和或处理太多的责任。这样的代码可以被分解为更小、更集中的函数。
对于代码中不太可能出现错误的区域,通常最好是放弃过多的 try-catch
语句,直接允许错误发生。这听起来可能违反直觉,但允许代码快速失败,实际上是让自己处于一个更好的位置。压缩错误可能会带来稳定的外观,但仍然会有潜在的问题。
允许错误在没有显式处理的情况下发生意味着当与应用程序监视工具如 BugSnag 或 Sentry 配合使用,你可以全局地拦截和记录错误,以供以后分析。这些工具可以让你看到应用程序中错误的实际位置,以便你能够修复它们,而不是盲目地忽略它们。
要理解 JavaScript 中的异步函数是什么,就必须理解 Promise 是什么。
promise
本质上是表示异步操作最终完成的对象。它们定义了将来要执行的操作,最终将 resolve
(成功)或 reject
(错误)。
例如,下面的代码显示了快速解析值的简单 Promise
。然后将该值传递给 then
回调函数:
// 这个 Promise 将 resolve
new Promise((resolve, reject) => {
const a = 10;
const b = 9;
resolve(a + b);
})
.then(result => {
console.log(result); // 19
});
下一个例子展示了在 Promise
中抛出的错误将如何由 Promise
catch
回调处理:
new Promise((resolve, reject) => {
const a = 10;
const b = 9;
throw new Error('manually thrown error')
resolve(a + b); // 这里没有执行到
})
.then(result => {
console.log(result); // 这个永远不会被执行
})
.catch(error => {
console.log('something went wrong', error) // Something went wrong [Error: manually thrown error]
})
你可以使用 reject
函数,而不是在 promise
中手动抛出错误。这个函数作为 Promise
的回调函数的第二个参数提供:
new Promise((resolve, reject) => {
const a = 10;
const b = 9;
reject('- manual rejection');
console.log(a + b); // 19
resolve(a + b);
})
.then(result => {
console.log(result); // 这个永远不会被执行
})
.catch(error => {
console.log('something went wrong', error) // something went wrong - manual rejection
})
你可能会在最后一个例子中注意到一些奇怪的东西。抛出错误和 reject
是有区别的。抛出错误将停止代码的执行,并将错误传递给最近的 catch
语句或终止程序。reject
一个 Promise
将调用 catch
回调函数,但如果有更多的代码要运行,它不会停止 Promise
的执行,除非对 reject
函数的调用带有返回关键字的前缀。这就是为什么 console.log(a + b);
即使 reject
,声明仍然被触发。要避免这种行为,只需使用 return reject(…)
提前结束执行即可。
try-catch
语句是一个很有用的工具,在程序员的职业生涯中肯定会用到。然而,任何不加选择地使用的方法都可能不是最好的解决方案。记住,你需要使用正确的工具和概念来解决特定的问题。例如,当你不希望发生任何异常时,通常不会使用 try-catch
语句。如果在这些情况下确实发生了错误,你可以在出现错误时进行识别和修复。