在 JavaScript 中处理特定的错误(考虑异常)

您将如何实现不同类型的错误,以便能够捕获特定的错误并让其他错误冒泡出现。.?

实现这一点的一种方法是修改 Error对象的原型:

Error.prototype.sender = "";




function throwSpecificError()
{
var e = new Error();


e.sender = "specific";


throw e;
}

捕获特定错误:

try
{
throwSpecificError();
}


catch (e)
{
if (e.sender !== "specific") throw e;


// handle specific error
}


你们还有别的选择吗?

105735 次浏览

正如下面的评论所指出的,这是 Mozilla 特有的,但是你可以使用“条件 catch”块。例如:

try {
...
throwSpecificError();
...
}
catch (e if e.sender === "specific") {
specificHandler(e);
}
catch (e if e.sender === "unspecific") {
unspecificHandler(e);
}
catch (e) {
// don't know what to do
throw e;
}

这使得 Java 中使用的类型化异常处理更加类似,至少在语法上是这样的。

要创建自定义异常,可以从 Error对象继承:

function SpecificError () {


}


SpecificError.prototype = new Error();


// ...
try {
throw new SpecificError;
} catch (e) {
if (e instanceof SpecificError) {
// specific error
} else {
throw e; // let others bubble up
}
}

一种不继承 Error的极简方法可能是抛出一个具有名称和消息属性的简单对象:

function throwSpecificError() {
throw {
name: 'SpecificError',
message: 'SpecificError occurred!'
};
}




// ...
try {
throwSpecificError();
} catch (e) {
if (e.name == 'SpecificError') {
// specific error
} else {
throw e; // let others bubble up
}
}

try-catch-finally.js

使用 Try-catch-finally. js,您可以使用匿名回调调用 _try函数,它将调用这个回调,您可以链接 .catch调用以捕获特定的错误,并使用 .finally调用以两种方式执行。

例子

_try(function () {
throw 'My error';
})
.catch(Error, function (e) {
console.log('Caught Error: ' + e);
})
.catch(String, function (e) {
console.log('Caught String: ' + e);
})
.catch(function (e) {
console.log('Caught other: ' + e);
})
.finally(function () {
console.log('Error was caught explicitly');
});

具有现代箭头函数和模板文字的示例

_try(() => {
throw 'My error';
}).catch(Error, e => {
console.log(`Caught Error: ${e}`);
}).catch(String, e => {
console.log(`Caught String: ${e}`);
}).catch(e => {
console.log(`Caught other: ${e}`);
}).finally(() => {
console.log('Error was caught explicitly');
});

我不喜欢这些解决方案,所以我自己做了一个。尝试接住终于。Js 非常酷,只是如果在尝试之前忘记了一个小的下划线(_) ,那么代码仍然可以正常运行,但是什么都不会被捕获!真恶心。

捕捉过滤器

我在代码中添加了一个 CatchFilter:

"use strict";


/**
* This catches a specific error. If the error doesn't match the errorType class passed in, it is rethrown for a
* different catch handler to handle.
* @param errorType The class that should be caught
* @param funcToCall The function to call if an error is thrown of this type
* @return {Function} A function that can be given directly to the `.catch()` part of a promise.
*/
module.exports.catchOnly = function(errorType, funcToCall) {
return (error) => {
if(error instanceof errorType) {
return funcToCall(error);
} else {
// Oops, it's not for us.
throw error;
}
};
};

现在我可以过滤了

现在我可以像 C # 或 Java 那样进行过滤:

new Promise((resolve, reject => {
<snip><snip>
}).catch(CatchFilter.catchOnly(MyError, err =>
console.log("This is for my error");
}).catch(err => {
console.log("This is for all of the other errors.");
});

用于导出的模块

/**
* Custom InputError
*/
class InputError extends Error {
/**
* Create InputError
* @param {String} message
*/
constructor(message) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}


/**
* Custom AuthError
*/
class AuthError extends Error {
/**
* Create AuthError
* @param {String} message
*/
constructor(message) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}


/**
* Custom NotFoundError
*/
class NotFoundError extends Error {
/**
* Create NotFoundError
* @param {String} message
*/
constructor(message) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}


module.exports = {
InputError: InputError,
AuthError: AuthError,
NotFoundError: NotFoundError
};

导入脚本:

const {InputError, AuthError, NotFoundError} = require(path.join(process.cwd(), 'lib', 'errors'));

用途:

function doTheCheck = () =>
checkInputData().then(() => {
return Promise.resolve();
}).catch(err => {
return Promise.reject(new InputError(err));
});
};

外部呼叫代码:

doTheCheck.then(() => {
res.send('Ok');
}).catch(err => {
if (err instanceof NotFoundError) {
res.status(404).send('Not found');
} else if (err instanceof AuthError) {
res.status(301).send('Not allowed');
} else if (err instanceof InputError) {
res.status(400).send('Input invalid');
} else {
console.error(err.toString());
res.status(500).send('Server error');
}
});

不幸的是,在 Javascript 中没有“正式”的方法来实现这个基本功能。我将分享我在不同的包中看到的三个最常见的解决方案,以及如何在现代 Javascript (es6 +)中实现它们,同时也分享它们的一些优缺点。

子类化 Error 类

对一个“ Error”实例进行子类化在 es6中变得更加容易:

class FileNotFoundException extends Error {
constructor(message) {
super(message);
// Not required, but makes uncaught error messages nicer.
this.name = 'FileNotFoundException';
}
}

完整的例子:

class FileNotFoundException extends Error {
constructor(message) {
super(message);
// Not required, but makes uncaught error messages nicer.
this.name = 'FileNotFoundException';
}
}


// Example usage


function readFile(path) {
throw new FileNotFoundException(`The file ${path} was not found`);
}


try {
readFile('./example.txt');
} catch (err) {
if (err instanceof FileNotFoundException) {
// Handle the custom exception
console.log(`Could not find the file. Reason: ${err.message}`);
} else {
// Rethrow it - we don't know how to handle it
// The stacktrace won't be changed, because
// that information is attached to the error
// object when it's first constructed.
throw err;
}
}

如果不喜欢将 this.name设置为硬编码的字符串,可以将其设置为 this.constructor.name,这将给出类的名称。这样做的好处是,自定义异常的任何子类都不需要同时更新 this.name,因为 this.constructor.name将是子类的名称。

子类异常的优点是,与一些替代解决方案相比,它们可以提供更好的编辑器支持(如自动完成)。您可以轻松地将自定义行为添加到特定的异常类型,例如附加函数、可选的构造函数参数等。在提供自定义行为或数据时,支持类型脚本也更容易。

有很多关于如何正确子类化 Error的讨论。例如,如果使用传送器,上述解决方案可能无法工作。有些人建议,如果可用的话,可以使用特定于平台的 captureStackTrace ()(不过我在使用它的时候并没有注意到错误有什么不同——也许它已经不再那么相关‍♂了)。阅读更多,请参阅 这个 MDN 页面和 这个 Stackoverflow 答案。

许多浏览器 API 采用这种方式并抛出自定义异常(可以看到 给你)

请注意,babel 并不十分支持这种解决方案。在翻译类语法时,他们必须做出某些权衡(因为不可能100% 准确地翻译它们) ,而且他们选择在翻译了巴别塔的类上破坏 instanceof检查。有些工具,比如 TypeScript,会间接地使用 babel,因此会遇到同样的问题,这取决于您如何配置 TypeScript 设置。如果你今天(2022年3月)在 TypeScript 的操场上使用默认设置运行它,它会显示“ false”:

class MyError extends Error {}
console.log(MyError instanceof Error);

2. 向 Error 添加区分属性

这个想法很简单。创建错误,向错误添加额外的属性(如“ code”) ,然后抛出它。

const error = new Error(`The file ${path} was not found`);
error.code = 'NotFound';
throw error;

完整的例子:

function readFile(path) {
const error = new Error(`The file ${path} was not found`);
error.code = 'NotFound';
throw error;
}


try {
readFile('./example.txt');
} catch (err) {
if (err.code === 'NotFound') {
console.log(`Could not find the file. Reason: ${err.message}`);
} else {
throw err;
}
}

当然,您可以创建一个 helper 函数来删除一些样板并确保一致性。

此解决方案的优点是不需要导出包可能抛出的所有可能异常的列表。您可以想象,如果您的包使用 NotFind 异常来指示某个特定函数无法找到预期的资源,那么情况会有多么尴尬。您需要添加一个 addUserToGroup ()函数,该函数在理想情况下会根据没有找到哪个资源而抛出 UserNotFind 或 GroupNotFind 异常。对于子类异常,您将面临一个棘手的决定。有了错误对象上的代码,您就可以这样做了。

这是路由节点的 fs 模块对异常的处理。如果您试图读取一个不存在的文件,它将抛出一个带有一些附加属性的 Error实例,例如 code,它将针对该特定异常将其设置为 "ENOENT"

3. 返回异常。

谁说你必须扔了它们?在某些情况下,返回出错的地方可能是最有意义的。

function readFile(path) {
if (itFailed()) {
return { exCode: 'NotFound' };
} else {
return { data: 'Contents of file' };
}
}

当处理大量异常时,这样的解决方案可能是最有意义的。这样做很简单,并且可以帮助自我记录哪些函数提供哪些异常,这使得代码更加健壮。缺点是它会给您的代码增加大量的膨胀。

完整的例子:

function readFile(path) {
if (Math.random() > 0.5) {
return { exCode: 'NotFound' };
} else {
return { data: 'Contents of file' };
}
}


function main() {
const { data, exCode } = readFile('./example.txt');


if (exCode === 'NotFound') {
console.log('Could not find the file.');
return;
} else if (exCode) {
// We don't know how to handle this exCode, so throw an error
throw new Error(`Unhandled exception when reading file: ${exCode}`);
}


console.log(`Contents of file: ${data}`);
}
main();

没有解决办法

其中一些解决方案让人感觉工作量很大。只抛出一个对象文字是很有诱惑力的,例如 throw { code: 'NotFound' }。将堆栈跟踪信息附加到错误对象。如果这些对象文字中的一个曾经错过并成为一个未捕获的异常,您将不会有一个堆栈跟踪来知道它发生在哪里或如何发生。一般来说,调试会困难得多。一些浏览器可能会在控制台中显示一个堆栈跟踪,如果这些对象没有被捕获,但这只是一个可选的便利,并不是所有的平台都提供这种便利,并不总是准确的,例如,如果这个对象被捕获并重新抛出,浏览器可能会给出错误的堆栈跟踪。

即将到来的解决方案

JavaScript 委员会正在研究一些提案,这些提案将使异常处理变得更好。这些提议将如何运作的细节仍在变化之中,并且正在积极讨论之中,所以在事情平息之前,我不会深入探讨太多细节,但下面是即将到来的事情的粗略体验:

未来最大的变化将是 模式匹配建议书,其目的是成为一个更好的“开关”。使用它,您可以轻松地使用简单的语法来匹配不同样式的错误。

下面是一些可能看起来像是这样的东西:

try {
...
} catch (err) {
match (err) {
// Checks if `err` is an instance of UserNotFound
when (${UserNotFound}): console.error('The user was not found!');


// Checks if it has a correct code property set to "ENOENT"
when ({ code: 'ENOENT' }): console.error('...');


// Handles everything else
else: throw err;
}
}

使用返回异常路由的模式匹配可以让你以一种与函数式语言非常相似的方式来处理异常。唯一缺少的是“要么”类型,但是 TypeScript 联合类型可以实现非常类似的角色。

const result = match (divide(x, y)) {
// (Please refer to the proposal for a more in-depth
// explanation of this syntax)
when ({ type: 'RESULT', value }): value + 1
when ({ type: 'DivideByZero' }): -1
}

还有关于将这种模式匹配语法引入 try-catch 语法的 一些早期的讨论,它允许您执行类似于下面这样的操作:

try {
doSomething();
} CatchPatten (UserNotFoundError & err) {
console.error('The user was not found! ' + err);
} CatchPatten ({ type: 'ENOENT' }) {
console.error('File not found!');
} catch (err) {
throw err;
}

更新

对于那些需要自我记录哪些函数抛出了哪些异常,以及确保这些自我记录保持诚实的方法的人,我之前在这个答案中推荐了 一个小包裹,它可以帮助您跟踪给定函数可能抛出的异常。虽然这个包可以完成这项工作,但现在我只是简单地推荐使用 TypeScript 和“返回您的异常”路由,以获得最大的异常安全性。在 TypeScript 联合类型的帮助下,您可以很容易地记录特定函数将返回的异常,TypeScript 可以帮助您保持这个文档的真实性,在出错时提供类型错误。

一个老问题,但是在现代的 JS (截至2021年底)中,我们可以通过在 catch 块中打开错误的原型构造函数来做到这一点,只需将它直接匹配到我们感兴趣的任何和所有错误类,而不是进行 instanceof检查,利用这样一个事实: 虽然 instanceof会匹配整个层次结构,但身份检查不会:

import { SomeError } from "library-that-uses-errors":
import MyErrors from "./my-errors.js";


try {
const thing = someThrowingFunction();
} catch (err) {
switch (err.__proto__.constuctor) {
// We can match against errors from libraries that throw custom errors:
case (SomeError): ...


// or our own code with Error subclasses:
case (MyErrors.SOME_CLASS_THAT_EXTENDS_ERROR): ..


// and of course, we can check for standard built-in JS errors:
case (TypeError): ...


// and finally, if we don't know what this is, we can just
// throw it back and hope something else deals with it.
default: throw err;
}
}

(当然,我们也可以使用 if/elseif/else,如果开关太“我讨厌在任何地方都必须使用 break”,这对很多人来说都是真的)