在 JavaScript 错误上提供错误代码的最佳方法是什么?

Aar*_*ius 12 javascript error-handling

我有一个抛出错误的库:

throw new Error('The connection timed out waiting for a response')

它可能会因多种不同的原因引发错误,并且用户很难以不同的方式以不同的方式处理错误而没有switching on error.message,这不是最佳的,因为该消息并不是真正用于编程决策。我知道很多人 subclass Error,但似乎过分了。相反,我正在考虑 (a)error.name用自定义名称覆盖:

const error = new Error('The connection timed out waiting for a response');
error.name = 'ConnectionTimeout';
throw error;
Run Code Online (Sandbox Code Playgroud)

或 (b) 设置error.code(不是标准属性):

const error = new Error('The connection timed out waiting for a response');
error.code = 'ConnectionTimeout';
throw error;
Run Code Online (Sandbox Code Playgroud)

有没有首选的方法?这些方法中的任何一种都令人不悦吗?这是我能找到的关于该主题的最接近的对话,但它似乎没有定论,并且可能与新约定过时了:https : //esdiscuss.org/topic/creating-your-own-errors

Tom*_*Tom 5

这是一个更大的答案(我自己的)的大幅增强的部分重印,其中大部分是不相关的。但我怀疑您正在寻找的是:

为理智的人提供的错误消息

考虑这个糟糕的(但常见的)保护代码:

function add( x, y ) {
  if(typeof x !== 'number')
    throw new Error(x + ' is not a number')
  
  if(typeof y !== 'number')
    throw new Error(y + ' is not a number')
  
  return x + y
}
Run Code Online (Sandbox Code Playgroud)

每次add使用不同的非数字调用时xerror.message都会有所不同:

add('a', 1)
//> 'a is not a number'

add({ species: 'dog', name: 'Fido' }, 1) 
//> '[object Object] is not a number'
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,我都做了同样的坏事:为x. 但错误信息却不同!这使得在运行时将这些情况分组在一起变得不必要的困难。我的例子甚至让人无法判断它是否有价值xy冒犯!

这些问题非常普遍地适用于您从本机代码和库代码中收到的错误。我的建议是,如果可以避免的话,不要在自己的代码中重复它们。

我发现的最简单的补救措施就是始终使用静态字符串作为错误消息,并花一些时间为自己建立约定。这就是我所做的。

我抛出的大多数异常分为两类:

  • 我希望使用的某些值令人反感
  • 我尝试的某些操作失败

我发现每个类别都有自己的规则,因此可以得到很好的服务。

令人反感的价值观

第一种情况,相关信息是:

  • 哪个数据点不好;我把这个称为主题
  • 一言以蔽之,为什么它不好;我称之为反对

与令人反感的值相关的所有错误消息都应该包括两个数据点,并且以足够一致的方式来促进流量控制(我认为这是您关心的问题),同时保持人类可以理解。理想情况下,您应该能够在grep文字消息的代码库中找到可能引发错误的每个位置(这对维护有很大帮助)。

根据这些准则,我将如何构建错误消息:

objection + space + topic
// e.g.
"unknown planetName"
Run Code Online (Sandbox Code Playgroud)

通常存在一组离散的反对意见

  • 缺失:未提供值
  • 未知:无法在数据库中找到价值以及其他无法解决的关键问题
  • unavailable:值已被使用(例如用户名)
  • 禁止:有时特定值是禁止的,尽管其他方面很好(例如,没有用户可能有用户名“root”)
  • 非字符串:值必须是 a String,但不是
  • 非数字:值必须是 a Number,但不是
  • 非日期:值必须是 a Date,但不是
  • ...基本 JS 类型的附加“non-[specific-type]”(虽然我从来不需要non-nullnon-undefined,但这些将是反对意见)
  • invalid:被开发社区严重过度使用;作为最后的选择;专门为语法上不可接受的值保留(例如zipCode = '__!!@'

我根据需要为各个应用程序补充了更专业的反对意见,但这组已经满足了我的大部分需求。

主题几乎总是出现在抛出的代码块中的文字变量名称为了帮助调试,我认为不要以任何方式转换变量名(例如更改字母大小写)非常重要。

该系统会产生如下错误消息:

'missing lastName'
'unknown userId'
'unavailable player_color'
'forbidden emailAddress'
'non-numeric x'
Run Code Online (Sandbox Code Playgroud)

这些消息始终是由单个空格分隔的两个“单词”,因此:let [objection, topic] = error.message.split(' '),然后您可以执行诸如找出要设置为错误状态的表单字段之类的操作。

让你的保护条款错误一丝不苟地诚实

现在你已经有了一套可以让你清晰地报告问题的标签,你必须诚实,否则这些标签将变得毫无价值。

不要这样做:

objection + space + topic
// e.g.
"unknown planetName"
Run Code Online (Sandbox Code Playgroud)

是的,确实undefined不是一个数值,但保护子句不会评估是否x是一个数字。这跳过了一个逻辑步骤,作为聪明的人,很容易跳过它。但如果你这样做了,你的许多错误都将是谎言,因此毫无用处。

实际上需要做很多工作才能使函数能够正确表达每个细微差别。如果我想“全力以赴”,我就必须这样做:

'missing lastName'
'unknown userId'
'unavailable player_color'
'forbidden emailAddress'
'non-numeric x'
Run Code Online (Sandbox Code Playgroud)

对于更有趣的值类型,例如社会保障号或电子邮件地址,如果您想让您的函数区分每个可能的失败场景,您将需要一两个额外的保护子句。这通常是矫枉过正。

通常,您可以只使用一个保护条款来确认该值一种可接受的事物,而不是费尽全力来确定您收到的是哪一种不可接受的事物。两者之间的区别在于懒惰的思维会导致不诚实的错误消息。

在我上面的例子中,如果我们这样做就可以了typeof x !== 'number';如果此函数无法区分undefined和任何其他非数字,我们可能不会失去任何有价值的东西。在某些情况下,您可能会关心;然后,“全力以赴”,一步一步缩小漏斗。

但由于您通常不会执行每个测试,因此您必须确保您选择的错误消息精确且准确地反映了您执行的一两个测试

操作失败

对于失败的操作,通常只有一个数据点:操作的名称。我使用这种格式:

operation + space + "failed"
// e.g.
"UserPreferences.save failed"
Run Code Online (Sandbox Code Playgroud)

通常,操作是例程的名称,与调用时的名称完全相同:

function add( x, y ) {
  if(x === undefined)
    throw new Error('non-numeric x') // bad!
}
Run Code Online (Sandbox Code Playgroud)

错误消息不是散文

错误消息看起来像面向用户的文本,这通常会激活我们大脑的散文写作部分。这意味着你要问自己这样的问题:

  • 我应该使用句子大小写吗?(例如以大写字母开头,以标点符号结尾)
  • 这里需要加逗号吗?或者应该是分号?
  • 是“电子邮件”还是“电子邮件”?那么“邮政编码”呢?
  • 我的“错误电话号码”消息是否应该包含正确的格式?

这些问题中的每一个,以及无数其他类似的问题,都是指明错误道路的路标。

错误消息不是推文,不是给下一个开发人员的注释,不是新手开发人员的教程,也不是您将向用户显示的帮助消息。错误消息是开发人员可读的错误代码,句点。我不建议使用数字常量(就像 Windows 那样)的唯一原因是没有人会维护一个单独的文档来将每个错误号映射到人类可读的解释。

因此,每当你内心的创意作者发出声音并提出帮助你编写“完美”的错误消息时,请将该作者踢出房间并锁上门。积极排除任何可能属于“编辑风格”的内容。如果两个理性的人能够以不同的方式回答这个问题,那么解决方案不是选择一个,而是两者都不做。

所以:

  • 没有标点符号,句号(哈!)
  • 100% 小写,主题操作必须混合大小写以反映实际标记的情况除外

这并不是纠正错误的唯一方法,但是这套约定确实可以轻松完成三件重要的事情:

  • 无需费力思考即可编写新的错误抛出代码;
  • 对异常做出明智的反应;和
  • 找到大多数可能引发的错误的来源。

你应该扔什么?

我们一直在讨论错误消息的约定,但这忽略了抛出什么样的东西的问题。你可以扔任何东西,但你应该扔什么?

我认为您应该只抛出 的实例Error,或者在适当的时候抛出它的子类之一(包括您自己的自定义子类)。即使您不同意我关于错误消息的所有建议,这也是事实。

两个最大的原因是本机浏览器代码和第三方代码抛出 real Error,因此如果您希望使用单个代码路径和工具集处理所有这些,则必须执行相同的操作,并且因为该类Error做了一些有用的事情(例如捕获堆栈跟踪)。

(剧透:在内置子类中,我怀疑您最常用的是TypeErrorSyntaxErrorRangeError。)

不管你扔的是哪种,我认为只有一种方法可以Error正确地扔:

function add( x, y ) {
  if(x === undefined) throw new Error('missing x')
  if(typeof x !== 'number') throw new Error('non-numeric x')
}
Run Code Online (Sandbox Code Playgroud)

如果您想在抛出错误之前将任意数据附加到错误中,您可以这样做:

operation + space + "failed"
// e.g.
"UserPreferences.save failed"
Run Code Online (Sandbox Code Playgroud)

如果您发现自己经常这样做,则表明您可以使用自定义子类来获取额外的参数并将它们自动连接到适当的位置。