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

时间:2017-11-17 03:36:06

标签: javascript error-handling

我有一个抛出错误的库:

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

由于几个不同的原因,它可能会抛出错误,用户很难以switch error.message Error以不同的方式以编程方式处理错误,这不是最佳的,因为消息不是真的用于程序化决策。我知道很多人都是error.name的子类,但它似乎太过分了。相反,我正在考虑(a)用自定义名称覆盖const error = new Error('The connection timed out waiting for a response'); error.name = 'ConnectionTimeout'; throw error;

error.code

或(b)设置const error = new Error('The connection timed out waiting for a response'); error.code = 'ConnectionTimeout'; throw error; (不是标准属性):

@Entity
@Immutable
public class <NAME>View {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", updatable = false, nullable = false)
private Long id;
@Version
@Column(name = "version")
private int version;

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

1 个答案:

答案 0 :(得分:1)

这是a much larger answer(我自己的)的大幅增强的部分重印,其中大部分不相关。但我怀疑您正在寻找的是:

给正常人的错误信息

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

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
}

每次使用不同的非数字 add 调用 x 时,error.message 都会不同:

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

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

在这两种情况下,我都做了同样的坏事:为 x 提供了一个不可接受的值。但是错误信息是不同的!这使得在运行时将这些案例组合在一起变得不必要地困难。我的例子甚至让人无法判断是 x 还是 y 的值有问题!

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

我发现的最简单的补救方法就是始终使用静态字符串作为错误消息,并考虑为自己建立约定。这就是我所做的。

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

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

我发现每个类别都有自己的规则。

令人反感的价值

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

  • 哪个数据点不好;我称之为主题
  • 为什么不好,一句话;我称之为反对

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

基于这些指导方针,以下是我构建错误消息的方法:

objection + space + topic
// e.g.
"unknown planetName"

通常有一组离散的反对意见

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

我会根据需要用更专业的反对意见补充个别应用,但这套已经涵盖了我的大部分需求。

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

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

'missing lastName'
'unknown userId'
'unavailable player_color'
'forbidden emailAddress'
'non-numeric x'

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

让你的保护条款错误严格诚实

既然您有一组标签可以让您清楚地报告问题,那么您必须诚实,否则这些标签将变得一文不值。

不要这样做:

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

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

要使函数能够正确表达每一个细微差别,实际上需要做很多工作。如果我想“全猪”,我必须做这样的事情:

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

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

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

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

但由于您通常不会执行所有测试,因此您必须确保您选择的错误消息准确、准确地反映了您所做的一两个测试 em> 执行。

失败的操作

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

operation + space + "failed"
// e.g.
"UserPreferences.save failed"

作为规则,operation 是与调用时完全相同的例程名称:

try {
  await API.updateUserProfile(newData)
  
} catch( error ) {
  throw new Error('API.updateUserProfile failed')
}

错误信息不是散文

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

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

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

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

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

所以:

  • 没有标点符号,句号(哈!)
  • 100% 小写,除非 topicoperation 必须混合大小写以反映实际标记

这不是避免错误的唯一方法,但是这组约定确实可以让您轻松完成三件重要的事情:

  • 编写新的抛出错误的代码,而不必费力思考;
  • 对异常做出智能反应;和
  • 找出大多数可能引发的错误的来源。

你应该扔什么

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

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

最大的两个原因是本机浏览器代码和第三方代码会抛出真正的 Error,因此如果您希望使用单个代码路径和工具集处理它们,您必须这样做,并且因为 { {1}} 类会做一些有用的事情(比如捕获堆栈跟踪)。

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

无论您抛出哪种类型,我认为只有一种方法可以正确抛出 RangeError

Error

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

throw new Error( message )

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