诸如调用 api 或验证用户输入的数据之类的操作在开发中非常常见,并且是可以给出正确结果或失败的函数的示例。一般来说,为了在 JavaScript(和其他语言)中控制它,我们通常使用并创建简单的异常。
它们似乎是控制我们正在开发的应用程序或程序可能出现的错误的最简单方法。然而,随着项目和团队的成长,开始出现需要我们做更多事情的场景。例如,在大型团队中,如果功能在指示它们是否可能失败时是明确的,那么这将有很大帮助,以便我们的同事能够预测和管理这些错误。
明确操作可能出现的“错误”类型不仅有助于使开发变得更加容易。它还将作为业务规则的文档。
在 javascript 中,我们有一些技术来实现这一点。为了超越理论,让我们考虑一个现实生活中的例子:在酒店预订应用程序中,用户预订房间,收到代码,然后必须付款。付款时,api 目前可以向我们展示以下 3 种“错误”场景:
el código de reserva no existe. el pago es rechazado. el código de reserva ya no es válido.
当我们为用户制作应用程序时,除了这两种情况外,您还应该考虑额外的一种情况:
立即学习“Java免费学习笔记(深入)”;
no hay conexión a internet. (o el servicio no esta disponible)
可以从应用程序的不同组件调用此函数,如果失败,必须向用户显示错误。
考虑到这个例子,让我们回顾一下如何处理它的一些可能性
自定义错误
异常在许多语言中都很常见,javascript 包含一些预定义的异常(例如 syntaxError)。在处理可能的错误时,一个好的做法是具体化并个性化它们。
在 JS 中创建异常只需使用保留字 throw 后跟我们想要的任何内容(如果你是这么读的话)。
function makeerror() { throw "error string" }
从这个意义上说,js 是非常宽容的,但是抛出不是 js 中的 error 类的后代的东西被认为是不好的做法。
class myerror extends error { constructor(message) { super(message); this.name = "myerror"; } } function makeerror() { throw myerror("") }
如您所见,错误类带有一个属性,它允许我们更详细地描述为什么我们要创建异常(并且我们可以添加我们想要的属性)。
回到我们举的例子的问题。通过应用自定义错误,我们可以控制在每种情况下要做什么。
class codeinvalid extends error {/*...*/} class paymentrejected extends error {/*...*/} class codeexpired extends error {/*...*/} class rederror extends error {/*...*/} async function apitransaction(code) { try { const r = await fetch(/* url */) const response = await r.json() .... if (response.message === "code invalid") { throw new codeinvalid() } // agregar más casos return response } catch (e) { throw new rederror() } } async function payreservation(code) { try { const paydata = await apitransaction(code) showresulttouser(paydata) return } catch (e) { if (e instanceof codeinvalid) { showmessagetocodeinvalid() return } else if (e instanceof paymentrejected) { // ... } else if (e instanceof codeexpired) { // ... } else (e instanceof rederror) { // ... } throw e } }
通过这个,我们不仅能够以不同的方式路由流程,而且还可以区分系统的内部错误(例如,我们在 payreservation 等中使用的某些内部依赖项的错误)与代表业务规则的错误。
这是一个非常好的选择,它满足了我们根据每种情况控制流程的目标,如果有人看到这个函数,他们就知道为什么它会失败。这样我们已经收获很多,但是使用这种方法我们必须考虑一些事情。
如果函数的异常未在 catch 内控制,则会进入“更高级别”。举个例子,如果你有函数 a,它调用 b,这又调用 c 并且 c 抛出异常. 受控制的这将转到 b,如果 b 不控制它,则继续直到 a 等。根据您的情况,这可能是个好消息。通过业务规则声明可能的错误最终可能会很乏味,因为必须检查所有功能是否存在可能的异常。
另一点需要考虑的是今天非常重视的开发者到期。尽管像 jsdoc 这样的工具允许您描述添加方法可能有异常,但编辑器无法识别它。另一方面,typescript 在编写或调用函数时无法识别这些异常。
[] **性能:* 抛出和处理异常对性能有(最小)影响(类似于使用 break)。尽管在像应用程序这样的环境中,影响几乎为零。
使用值 result(或 either)封装错误
如果我们看一下前面的案例,我们创建的异常并不是由于“无法修复”的错误,而是业务规则的一部分。当异常变得普遍时,它们就不再是真正的异常情况,而是为此而设计的。我们可以将“成功”和“错误”状态封装在单个对象中,而不是抛出异常,如下所示。
const responsemodel = { isok: true, data: {}, errorname: '' }
如果我们使用 typescript(或 d.ts 使用 jsdoc),我们可以像这样定义类型。
type response<t,n extends string> = okresponse<t> | errorresponse<n> interface okresponse<t> { isok: true, data: t } interface errorresponse<n> { isok: false, errorname: n }
将其应用到我们的示例中。如果现在我们的 payreservation 函数返回此对象而不是异常,则使用 jsdoc 或 typescript 我们可以指定可以采用的结果类型(从现在开始,为了简单起见,我将把示例放在 typescript 中)。
这有助于团队提前了解函数可能返回哪些错误。
type apitransactionresponse = response<paymentdata, 'paymentpartnererror'|'paymentrejected'|'codeinvalid'|'rederror'> function apitransaction(code: number) : promise<apitransactionresponse> { //... } async function payreservation(code) { const paydata = await apitransaction(code) if (paydata.isok) { showresulttouser(paydata) } else if (paydata.errorname === 'codeinvalid') { showmessagetocodeinvalid() return } else if (paydata.errorname === 'paymentrejected') { // ... } else if (paydata.errorname === 'codeinvalid') { // ... } else (paydata.errorname === 'rederror') { // ... } }
通过应用此方法,我们获得了该方法(有例外情况)的优点,并且在开发时,编辑器将显示有关可能发生的不同“错误”情况的信息。
事实上,这种类型的概念在编程中已经存在很长时间了,在许多函数式语言中他们没有例外,他们使用这种类型的数据来处理错误,今天许多语言都实现了它。例如,在 rust 和 dart 中,result 类本身就存在,kotlin arrow 库也添加了它。
关于如何使用和实现结果有一定的标准,这样我们的代码对于新开发人员来说更容易理解,我们可以依赖这些约定。
结果可以专门表示成功或错误状态(不能同时是两种状态),并且允许在不抛出异常的情况下使用这两种状态。
class result<r, e extends string> () { #isok = true get value() { return this.#value } get error() { return this.#error } get isok() { return this.#isok } get iserror() { return this.#iserror } constructor(value, isok : boolean) { if ( isok) { this.#value = value } else { this.#error = value } this.#isok = isok } static ok<t>(value: t) : result<t, never> { return new result(value, true) } static error<e>(error: e) : result<never, e> { return new result(value, false) } }
该示例使用类,但不是必需的,还有更简单的实现,我通常将一个实现带到我认为可能需要它的项目中,我留下链接以防您想查看它和/或使用它。
如果我们就这样保留它,那么相对于我们之前创建的对象,我们就不会获得太多收益。这就是为什么很高兴知道它通常实现更多方法
例如,在发生错误时返回默认值的 getorelse 方法。
class result<r, e> () { //... function getorelse(defvalue: r) { if (this.isok) { return this.#value } return defvalue } //... }
并折叠以功能性地处理成功/失败流程。
class result<r, e> () { //... function fold(fnok: (arg: r) => any, fnerror: (arg: e) => any) { if (this.isok) { return fnok(this.value) } return fnerror(this.error) } //... }
您还可以使用 either 找到有关错误处理的信息。结果将是具有更大上下文的 either,either 的值为右 (右) 和左 (左)。正如英语中的 right 也用来表示某件事是正确的,它通常具有正确的值,而错误在左侧,但情况不一定如此,结果相对于这是正确值和错误值。
将其应用到我们的示例中,payreservation 看起来像这样:
type apitransactionresponse = result<paymentdata, 'codeinvalid'|'paymentrejected'|'codeexpired'|'rederror'> function apitransaction(code: number) : promise<apitransactionresponse> { //... return new result.ok(paymentdata) // or ... return new result.error(descriptionerror) } async function payreservation(code) { const paydataresult = await apitransaction(code) return paydataresult.fold( (paydata) => { return showresulttouser(paydata) }, (e) => { if (e === 'codeinvalid') { return showmessagetocodeinvalid() } else if (e === 'paymentrejected') { //... } else if (e === 'codeexpired') { //... } else if (e === 'rederror') { //... } } ) }
[*] 一个好的做法是为错误建立一个基本数据类型,在示例中使用字符串,但理想情况下它应该有一个更定义的形式,例如可以添加更多数据的命名对象,例如,你可以在这里看到一个例子
乍一看,添加类似乎比其他任何事情都更加过度设计。但结果是一个广泛使用的概念,维护约定可以帮助您的团队更快地捕获它,并且是加强错误处理的有效方法。
使用此选项,我们明确描述函数可能出现的“错误”,我们可以根据错误类型控制应用程序的流程,我们在调用函数时从编辑器获得帮助,最后我们留下错误异常系统中的案例。
尽管有这些优点,但在实施之前还必须考虑一些要点。正如我在本节开头提到的,result 在许多语言中都是原生的,但在 js 中却不是,因此通过实现它,我们添加了一个额外的抽象。另一点需要考虑的是我们所处的场景,并非所有应用程序都需要如此多的控制(例如,在广告活动的登陆页面上,我看不到实现结果的意义)。是否可以充分利用所有潜力是值得评估的,否则只是额外的负担。
简而言之,处理错误不仅可以提高代码质量,还可以通过提供可预测且记录良好的工作流程来提高团队协作。结果和自定义异常是工具,如果使用得当,有助于提高代码的可维护性和健壮性。
ts 中的额外内容
在 typescript 中,我们可以从 result 中获得额外的好处,以确保涵盖所有错误情况:
function typeCheck(_:never) {} async function payReservation(code) { const payDataResult = await apiTransaction(code) return payDataResult.fold( (payData) => { return showResultToUser(payData) }, (e) => { if (e === 'CodeInvalid') { return showMessageToPartnerError() } elseif (e === 'PaymentRejected') { //... } elseif (e === 'CodeExpired') { //... } elseif (e === 'RedError') { //... } else { typeCheck(e) } } ) }
typecheck 函数旨在验证 if/else if 中检查了 e 的所有可能值。
在此存储库中,我留下了更多细节。