深入了解JavaScript/Node.js中的错误处理
什么是错误?
错误是用户执行的非法操作,导致程序异常工作。
错误类型: 错误可以广泛分类为
- 操作错误: 在程序执行任务时发生的错误。例如,网络故障就是操作错误。
- 开发者错误: 这种错误发生在开发者犯了一个错误的情况下。这种错误的主要示例是无效的输入。在这些情况下,程序不应继续运行,而应崩溃并提供有用的描述,以便开发者可以解决他们的错误。
抛出错误: 通常可以使用 throw 关键字来处理输入错误。
示例:
JavaScript
function doTask(amount) {
if (typeof amount !== 'number')
throw new Error('amount must be a number')
return amount / 2
}
doTask("This will generate an error")
输出:
正如我们所观察到的,当程序运行时,我们得到一个错误信息,错误信息显示“金额必须为数字。”
因此,我们可以限制程序可以接受的输入类型,并停止程序的执行,以避免生成任何意外或错误的输出。
Javascript中的本地错误类: 还有六个继承自基本错误类Error的本地错误构造函数,它们分别是:
- ReferenceError
- EvalError
- SyntaxError
- RangeError
- TypeError
- URIError
这些错误构造函数主要用于原生JavaScript API和功能。
1.引用错误: 当我们使用未定义的变量或尚未定义的变量时,会抛出ReferenceError类型的错误。
语法:
node -p "thisVariableIsNotDefined"
示例:
Javascript
const defined = "This variable is defined";
console.log(defined);
console.log(notDefined);
// There is no such declaration for
// a variable named notDefined and
// thus results in a reference error
输出:
这是因为没有名为 thisVariableIsNotDefined 的变量,因此抛出了一个 ReferenceError 错误。
2. EvalError: EvalError对象指示与全局eval()函数相关的错误。此异常不再由JavaScript抛出。但是,EvalError对象仍然保留以保持兼容性。
由于错误不再由javascript抛出,我们可以手动抛出 EvalError 对象。
语法:
throw new EvalError('message',
'file_where_error_occurs', 'line_number')
示例: 一个非常常见的 EvalError 的示例是我们试图将一个数字除以0,下面是模拟相同情况的代码片段,注意默认情况下JavaScript引擎不会抛出EvalError,所以我们需要手动抛出错误。先看一下下面的示例,然后我们将讨论代码片段中究竟发生了什么。
JavaScript
try {
const a = 7;
const b = 0;
if (b === 0)
throw new EvalError("Cannot divide a number by 0");
let quot = eval(a / b);
console.log(`Quotient = ${quot}`);
} catch (err) {
console.log("Error is of Type : " + err.name);
console.log("Error Message : " + err.message);
}
输出结果:
我们声明了两个变量,分别是 a 和 b ,并用一些数字初始化它们。然后我们检查变量b是否等于0,如果是的话,抛出一个新的 EvalError ,然后在try/catch块中处理它,我们在这里打印错误类型和错误信息。
3. SyntaxError: eval()函数将表示为字符串的JavaScript代码评估并返回其完成值。
为了理解 eval() 的用法,看下面的示例:
正如你可以看到的 操作符 ,并且 两个操作数 ,是 eval() 函数所必需的。
现在让我们看一下,如果你在 eval() 函数中不传递操作符时会发生什么:
我们可以观察到SyntaxError的发生,因为eval()函数需要一个操作符和两个操作数,所以它会抛出一个错误;因为找不到操作数。
4. RangeError: 当遇到越界(超出界限)的值时,会抛出RangeError错误。例如,在银行应用程序中,账户余额不能为负数。
Javascript
try {
const a = 7;
const b = 0;
if (b === 0)
throw new EvalError("Cannot divide a number by 0");
let quot = eval(a / b);
console.log(`Quotient = ${quot}`);
} catch (err) {
console.log("Error is of Type : " + err.name);
console.log("Error Message : " + err.message);
}
输出:
上面的代码块非常容易理解,它的作用是生成一个随机的三位数,但是请注意if条件。我们已经指定只允许0到99之间的值,由于random()函数生成的是三位数,所以会出现范围错误。
5. TypeError: TypeError对象表示在执行操作时发生错误,通常(但不限于)是因为值的类型不符合预期。考虑以下示例,我们编写一个函数来添加两个数字,并期望传递给函数的参数都是数字类型,如果不是,则抛出TypeError。
Javascript
const num1 = 2;
const num2 = "This is a string";
const add = (var1, var2) => {
try {
if (typeof var1 !== "number"
|| typeof var2 !== "number")
throw new TypeError(
"Only numeric values are allowed");
return var1 + var2;
} catch (err) {
console.log(err.message);
}
};
add(num1, num2);
输出:
执行上面的代码块将导致 TypeError ,因为传递的参数之一是字符串类型,显然不是一个数字。
6. URIError: 如果URI的编码或解码失败,将出现URIError。这个错误是因为在代码中的某个地方URI的编码或解码失败。
考虑以下代码片段中的代码行:
首先是有效的URI编码示例:
以下是另一个示例,我们传递了一个无效的编码字符串,导致了URIError错误,输出字符串 ‘URI Malformed’ :
我们还可以验证错误对象,并检查错误对象是否是特定类的实例。
考虑下面的示例:
首先,我们检查错误是否是 Error 类的一个实例,返回true,然后我们检查 SyntaxError 是否是 Error 类的一个实例,返回true,因为 SyntaxError 是从 Error 类继承的。最后,我们检查 SyntaxError 是否是 ReferenceError 类的一个实例,返回false,因为 SyntaxError 是从 Error 类继承的,而不是 ReferenceError 类。
自定义错误: 让我们更新 doTask 函数,当输入的数字是奇数时抛出自定义错误,这是 doTask 函数的实现
Javascript
function doTask(amount) {
if (typeof amount !== 'number')
throw new TypeError('amount must be a number')
if (amount <= 0)
throw new RangeError(
'amount must be greater than zero')
if (amount % 2) {
const err = Error('amount must be even')
err.code = 'ERR_NUM_MUST_BE_EVEN'
throw err
}
return amount / 2
}
doTask(3)
运行上述代码片段后,我们在输出窗口中得到的结果如下:
这是一种定义自定义错误的方法,另一种方法是通过继承 Error 类来创建自定义错误。
下面是一个自定义错误类的实现。
Javascript
class OddError extends Error {
constructor (varName = '') {
super(`${varName} must be even`)
}
get name () { return 'OddError' }
}
function doTask (amount) {
if (typeof amount !== 'number')
throw new TypeError('amount must be a number')
if (amount <= 0)
throw new RangeError('amount must be greater than zero')
if (amount % 2) throw new OddError('amount')
return amount / 2
}
doTask(3)
在执行上述代码片段后,在输出窗口中会得到类似于以下的输出内容:
我们可以在自定义错误类中添加 code 属性;我们可以对 OddError 类进行更改,做如下操作:
Javascript
class OddError extends Error {
constructor(varName = '') {
super(varName + ' must be even')
this.code = 'ERR_MUST_BE_EVEN'
}
get name() {
return 'OddError [' + this.code + ']'
}
}
当我们使用上述更新的自定义错误类时,在错误发生时也会在输出中获得错误代码 –
使用Try/Catch块进行错误处理:
当在普通同步函数中抛出错误时,可以使用try/catch块进行处理。
使用前面章节中的相同代码,我们将对doTask(3)函数调用进行try/catch块包装:
Javascript
try {
const result = doTask(3)
console.log('Result : ', result)
} catch (err) {
console.error('Error caught : ', err)
}
执行这个更新的代码将导致以下结果:
在这种情况下,我们控制了错误输出到终端,但是使用这种模式,我们也可以根据情况应用任何错误处理措施。
让我们将传递给doTask的参数更新为有效输入:
Javascript
try {
const result = doTask(4)
console.log('result', result)
} catch (err) {
console.error('Error caught: ', err)
}
这将导致以下输出:
拒绝: 在同步上下文中的 抛出异常 被称为异常。当一个 promise 被拒绝时,它表示一个 异步错误 。可以将异常和拒绝看作是同步错误和异步错误的两种思考方式。
假设 doTask 有一些异步工作要做,我们可以使用基于回调的 API 或者使用基于 promise 的 API(即使是基于 async/await 的也是基于 promise 的)。
让我们将 doTask 转换为返回一个解析为值的 promise,如果有错误则拒绝:
Javascript
function doTask(amount) {
return new Promise((resolve, reject) => {
if (typeof amount !== 'number') {
reject(new TypeError('amount must be a number'))
return
}
if (amount <= 0) {
reject(new RangeError('amount must be greater than zero'))
return
}
if (amount % 2) {
reject(new OddError('amount'))
return
}
resolve(amount / 2)
})
}
doTask(3)
但是等等,拒绝被搁置了,因为Promises必须使用try / catch方法来捕获拒绝,而到目前为止我们还没有附加一个catch处理程序。让我们将doTask调用修改为以下内容:
Javascript
doTask(3)
.then((result) => {
console.log('result', result)
})
.catch((err) => {
if (err.code === 'ERR_AMOUNT_MUST_BE_NUMBER') {
console.error('wrong type')
} else if (err.code === 'ERRO_AMOUNT_MUST_EXCEED_ZERO') {
console.error('out of range')
} else if (err.code === 'ERR_MUST_BE_EVEN') {
console.error('cannot be odd')
} else {
console.error('Unknown error', err)
}
})
输出:
当我们使用有效的值调用 doTask 函数时,它会解析并打印结果。
异步的try/catch: async/await语法支持对拒绝的try/catch。换句话说,我们可以在异步基于promise的API上使用try/catch,而不是像在下一节中使用then和catch处理程序,让我们创建一个名为run的异步函数,并重新引入与调用doTask同步形式时使用的相同的try/catch模式:
Javascript
async function run() {
try {
const result = await doTask(3)
console.log('result', result)
} catch (err) {
if (err instanceof TypeError) {
console.error('wrong type')
} else if (err instanceof RangeError) {
console.error('out of range')
} else if (err.code === 'ERR_MUST_BE_EVEN') {
console.error('cannot be odd')
} else {
console.error('Unknown error', err)
}
}
}
run()
唯一的区别是,除了将try/catch包装在一个异步函数中,我们还使用await关键字来等待doTask(3)的结果,这样异步函数就可以自动处理Promise了。由于3是一个奇数,doTask返回的Promise会调用reject方法,并传递我们自定义的OddError对象,catch块会识别code属性,然后输出 cannot be odd :
这个函数的工作方式与基于promise的方法相同,唯一的区别是它是通过异步地使用 Try/Catch 块来实现的。
始终将错误处理好放在代码的最顶层,例如-在NodeJS中app.js是应用程序的入口点,所有错误都应该在这里处理。
错误可以通过重新抛出错误传递到最顶层,考虑下面的代码片段
Javascript
const parent_function = () => {
try {
child_function()
} catch (err) {
console.log("All Errors are handled"
+ "here in the parent function");
console.log(`Type of Error :
{err.name}\nError Message :{err.message}`)
}
}
const child_function = () => {
try {
throw new Error("Error From Child Function")
} catch (err) {
throw new Error(err.message);
}
}
parent_function()
请将 parent_function 视为 程序的入口点,也请注意我们在 parent_function 中处理了 在 child_function 中发生的错误。
也许这有点令人困惑,但是当你看到输出结果时,你会明白上面的代码片段的作用。
极客教程