为什么这题用不同问题与解决问题的方法同时产生会产生两组不同数量的解

NodeJS的错误处理让人痛苦在很长的┅段时间里,大量的错误被放任不管但是要想建立一个健壮的Node.js程序就必须正确的处理这些错误,而且这并不难学如果你实在没有耐心,那就直接绕过长篇大论跳到“总结”部分吧

这篇文章会回答NodeJS初学者的若干问题:

  • 我写的函数里什么时候该抛出异常,什么时候该传给callback, 什么时候触发EventEmitter等等
  • 我的函数对参数该做出怎样的假设?我应该检查更加具体的约束么例如参数是否非空,是否大于零是不是看起来潒个IP地址,等等等
  • 我该如何处理那些不符合预期的参数?我是应该抛出一个异常还是把错误传递给一个callback。
  • 我该怎么在程序里区分不同嘚异常(比如“请求错误”和“服务不可用”)
  • 我怎么才能提供足够的信息让调用者知晓错误细节。
  • 我该怎么处理未预料的出错我是應该用 try/catchdomains 还是其它什么方式呢

这篇文章可以划分成互相为基础的几个部分:

  • 背景:希望你所具备的知识。
  • 操作失败和程序员的失误:介紹两种基本的异常
  • 编写新函数的实践:关于怎么让函数产生有用报错的基本原则。
  • 编写新函数的具体推荐:编写能产生有用报错的、健壯的函数需要的一个检查列表
  • 例子:connect函数为例的文档和序言
  • 总结:全文至此的观点总结。
  • 附录: Error对象属性约定:用标准方式提供一个屬性列表以提供更多信息。
    • 你已经熟悉了JavaScript、Java、 Python、 C++ 或者类似的语言中异常的概念而且你知道抛出异常和捕获异常是什么意思。
    • 你熟悉怎麼用NodeJS编写代码你使用异步操作的时候会很自在,并能用callback(err,result)模式去完成异步操作你得知道下面的代码不能正确处理异常的原因是什么。[脚紸1]

    你还要熟悉三种传递错误的方式:

    • 把错误传给一个callback这个函数正是为了处理异常和处理异步操作返回结果的。

    接下来我们会详细讨论这幾种方式这篇文章不假设你知道任何关于domains的知识。

    最后你应该知道在JavaScript里,错误和异常是有区别的错误是Error的一个实例。错误被创建并苴直接传递给另一个函数或者被抛出如果一个错误被抛出了那么它就变成了一个异常[脚注2]。举个例子:

    但是使用一个错误而不抛出也是鈳以的

    这种用法更常见因为在NodeJS里,大部分的错误都是异步的实际上,try/catch唯一常用的是在JSON.parse和类似验证用户输入的地方接下来我们会看到,其实很少要捕获一个异步函数里的异常这一点和Java,C++以及其它严重依赖异常的语言很不一样。

    操作失败和程序员的失误

    把错误分成两夶类很有用[脚注3]:

    • 操作失败是正确编写的程序在运行时产生的错误它并不是程序的Bug,反而经常是其它问题:系统本身(内存不足或者打開文件数过多)系统配置(没有到达远程主机的路由),网络问题(端口挂起)远程服务(500错误,连接失败)例子如下:
    • 程序员失誤 是程序里的Bug。这些错误往往可以通过修改代码避免它们永远都没法被有效的处理。
    • 调用异步函数没有指定回调
    • 该传对象的时候传了一個字符串
    • 该传IP地址的时候传了一个对象

    人们把操作失败和程序员的失误都称为“错误”但其实它们很不一样。操作失败是所有正确的程序应该处理的错误情形只要被妥善处理它们不一定会预示着Bug或是严重的问题。“文件找不到”是一个操作失败但是它并不一定意味着哪里出错了。它可能只是代表着程序如果想用一个文件得事先创建它

    与之相反,程序员失误是彻彻底底的Bug这些情形下你会犯错:忘记驗证用户输入,敲错了变量名诸如此类。这样的错误根本就没法被处理如果可以,那就意味着你用处理错误的代码代替了出错的代码

    这样的区分很重要:操作失败是程序正常操作的一部分。而由程序员的失误则是Bug

    有的时候,你会在一个Root问题里同时遇到操作失败和程序员的失误HTTP服务器访问了未定义的变量时奔溃了,这是程序员的失误当前连接着的客户端会在程序崩溃的同时看到一个ECONNRESET错误,在NodeJS里通瑺会被报成“Socket Hang-up”对客户端来说,这是一个不相关的操作失败, 那是因为正确的客户端必须处理服务器宕机或者网络中断的情况

    类似的,洳果不处理好操作失败, 这本身就是一个失误举个例子,如果程序想要连接服务器但是得到一个ECONNREFUSED错误,而这个程序没有监听套接字上的 error倳件,然后程序崩溃了这是程序员的失误。连接断开是操作失败(因为这是任何一个正确的程序在系统的网络或者其它模块出问题时都会經历的)如果它不被正确处理,那它就是一个失误

    理解操作失败和程序员失误的不同, 是搞清怎么传递异常和处理异常的基础。明白了這点再继续往下读

    就像性能和安全问题一样,错误处理并不是可以凭空加到一个没有任何错误处理的程序中的你没有办法在一个集中嘚地方处理所有的异常,就像你不能在一个集中的地方解决所有的性能问题你得考虑任何会导致失败的代码(比如打开文件,连接服务器Fork子进程等)可能产生的结果。包括为什么出错错误背后的原因。之后会提及但是关键在于错误处理的粒度要细,因为哪里出错和為什么出错决定了影响大小和对策

    你可能会发现在栈的某几层不断地处理相同的错误。这是因为底层除了向上层传递错误上层再向它嘚上层传递错误以外,底层没有做任何有意义的事情通常,只有顶层的调用者知道正确的应对是什么是重试操作,报告给用户还是其咜但是那并不意味着,你应该把所有的错误全都丢给顶层的回调函数因为,顶层的回调函数不知道发生错误的上下文不知道哪些操莋已经成功执行,哪些操作实际上失败了

    我们来更具体一些。对于一个给定的错误你可以做这些事情:

    • 直接处理。有的时候该做什么很清楚如果你在尝试打开日志文件的时候得到了一个ENOENT错误,很有可能你是第一次打开这个文件你要做的就是首先创建它。更有意思的例孓是你维护着到服务器(比如数据库)的持久连接,然后遇到了一个“socket hang-up”的异常这通常意味着要么远端要么本地的网络失败了。很多時候这种错误是暂时的所以大部分情况下你得重新连接来解决问题。(这和接下来的重试不大一样因为在你得到这个错误的时候不一萣有操作正在进行)
    • 把出错扩散到客户端。如果你不知道怎么处理这个异常最简单的方式就是放弃你正在执行的操作,清理所有开始的然后把错误传递给客户端。(怎么传递异常是另外一回事了接下来会讨论)。这种方式适合错误短时间内无法解决的情形比如,用戶提交了不正确的JSON你再解析一次是没什么帮助的。
    • 重试操作对于那些来自网络和远程服务的错误,有的时候重试操作就可以解决问题比如,远程服务返回了503(服务不可用错误)你可能会在几秒种后重试。如果确定要重试你应该清晰的用文档记录下将会多次重试,偅试多少次直到失败,以及两次重试的间隔另外,不要每次都假设需要重试如果在栈中很深的地方(比如,被一个客户端调用而那个愙户端被另外一个由用户操作的客户端控制),这种情形下快速失败让客户端去重试会更好如果栈中的每一层都觉得需要重试,用户最終会等待更长的时间因为每一层都没有意识到下层同时也在尝试。
    • 直接崩溃对于那些本不可能发生的错误,或者由程序员失误导致的錯误(比如无法连接到同一程序里的本地套接字)可以记录一个错误日志然后直接崩溃。其它的比如内存不足这种错误是JavaScript这样的脚本語言无法处理的,崩溃是十分合理的(即便如此,在child_process.exec这样的分离的操作里得到ENOMEM错误,或者那些你可以合理处理的错误时你应该考虑這么做)。在你无计可施需要让管理员做修复的时候你也可以直接崩溃。如果你用光了所有的文件描述符或者没有访问配置文件的权限这种情况下你什么都做不了,只能等某个用户登录系统把东西修好
    • 记录错误,其他什么都不做有的时候你什么都做不了,没有操作鈳以重试或者放弃没有任何理由崩溃掉应用程序。举个例子吧你用DNS跟踪了一组远程服务,结果有一个DNS失败了除了记录一条日志并且繼续使用剩下的服务以外,你什么都做不了但是,你至少得记录点什么(凡事都有例外如果这种情况每秒发生几千次,而你又没法处悝那每次发生都记录可能就不值得了,但是要周期性的记录)
    (没有办法)处理程序员的失误

    对于程序员的失误没有什么好做的。从萣义上看一段本该工作的代码坏掉了(比如变量名敲错),你不能用更多的代码再去修复它一旦你这样做了,你就使用错误处理的代碼代替了出错的代码

    有些人赞成从程序员的失误中恢复,也就是让当前的操作失败但是继续处理请求。这种做法不推荐考虑这样的凊况:原始代码里有一个失误是没考虑到某种特殊情况。你怎么确定这个问题不会影响其他请求呢如果其它的请求共享了某个状态(服務器,套接字数据库连接池等),有极大的可能其他请求会不正常

    典型的例子是REST服务器(比如用Restify搭的),如果有一个请求处理函数抛絀了一个ReferenceError(比如变量名打错)。继续运行下去很有肯能会导致严重的Bug而且极其难发现。例如:

    1. 一些请求间共享的状态可能会被变成nullundefined戓者其它无效值,结果就是下一个请求也失败了
    2. 数据库(或其它)连接可能会被泄露,降低了能够并行处理的请求数量最后只剩下几個可用连接会很坏,将导致请求由并行变成串行被处理
    3. 更糟的是, postgres 连接会被留在打开的请求事务里这会导致 postgres “持有”表中某一行的旧徝,因为它对这个事务可见这个问题会存在好几周,造成表无限制的增长后续的请求全都被拖慢了,从几毫秒到几分钟[脚注4]虽然这個问题和postgres 紧密相关,但是它很好的说明了程序员一个简单的失误会让应用程序陷入一种非常可怕的状态
    4. 连接会停留在已认证的状态,并苴被后续的连接使用结果就是在请求里搞错了用户。
    5. 套接字会一直打开着一般情况下 NodeJS 会在一个空闲的套接字上应用两分钟的超时,但這个值可以覆盖这将会泄露一个文件描述符。如果这种情况不断发生程序会因为用光了所有的文件描述符而强退。即使不覆盖这个超時时间客户端会挂两分钟直到“hang-up” 错误的发生。这两分钟的延迟会让问题难于处理和调试
    6. 很多内存引用会被遗留。这会导致泄露进洏导致内存耗尽,GC需要的时间增加最后性能急剧下降。这点非常难调试而且很需要技巧与导致造成泄露的失误联系起来。

    最好的从失誤恢复问题与解决问题的方法同时产生是立刻崩溃你应该用一个restarter 来启动你的程序,在奔溃的时候自动重启如果restarter 准备就绪,崩溃是失误來临时最快的恢复可靠服务问题与解决问题的方法同时产生

    奔溃应用程序唯一的负面影响是相连的客户端临时被扰乱,但是记住:

    • 从定義上看这些错误属于Bug。我们并不是在讨论正常的系统或是网络错误而是程序里实际存在的Bug。它们应该在线上很罕见并且是调试和修複的最高优先级。
    • 上面讨论的种种情形里请求没有必要一定得成功完成。请求可能成功完成可能让服务器再次崩溃,可能以某种明显嘚方式不正确的完成或者以一种很难调试的方式错误的结束了。
    • 在一个完备的分布式系统里客户端必须能够通过重连和重试来处理服務端的错误。不管 NodeJS
      应用程序是否被允许崩溃网络和系统的失败已经是一个事实了。
    • 如果你的线上代码如此频繁地崩溃让连接断开变成了問题那么正真的问题是你的服务器Bug太多了,而不是因为你选择出错就崩溃

    如果出现服务器经常崩溃导致客户端频繁掉线的问题,你应該把经历集中在造成服务器崩溃的Bug上把它们变成可捕获的异常,而不是在代码明显有问题的情况下尽可能地避免崩溃调试这类问题最恏问题与解决问题的方法同时产生是,把 NodeJS 配置成出现未捕获异常时把内核文件打印出来在 GNU/Linux 或者 基于 illumos 的系统上使用这些内核文件,你不仅查看应用崩溃时的堆栈记录还可以看到传递给函数的参数和其它的 JavaScript 对象,甚至是那些在闭包里引用的变量即使没有配置 code dumps,你也可以用堆栈信息和日志来开始处理问题

    最后,记住程序员在服务器端的失误会造成客户端的操作失败还有客户端必须处理好服务器端的奔溃囷网络中断。这不只是理论而是实际发生在线上环境里。

    我们已经讨论了如何处理异常那么当你在编写新的函数的时候,怎么才能向調用者传递错误呢

    最最重要的一点是为你的函数写好文档,包括它接受的参数(附上类型和其它约束)返回值,可能发生的错误以忣这些错误意味着什么。 如果你不知道会导致什么错误或者不了解错误的含义那你的应用程序正常工作就是一个巧合。 所以当你编写噺的函数的时候,一定要告诉调用者可能发生哪些错误和错误的含义

    • throw以同步的方式传递异常--也就是在函数被调用处的相同的上下文。如果调用者(或者调用者的调用者)用了try/catch则异常可以捕获。如果所有的调用者都没有用那么程序通常情况下会崩溃(异常也可能会被domains或鍺进程级的uncaughtException捕捉到,详见下文)
    • Callback 是最基础的异步传递事件的一种方式。用户传进来一个函数(callback)之后当某个异步操作完成后调用这个callback。通常 callback 会以callback(err,result)的形式被调用这种情况下, err和result必然有一个是非空的取决于操作是成功还是失败。
    • 更复杂的情形是函数没有用 Callback 而是返回一個 EventEmitter 对象,调用者需要监听这个对象的
      error事件这种方式在两种情况下很有用。
    • 当你在做一个可能会产生多个错误或多个结果的复杂操作的时候比如,有一个请求一边从数据库取数据一边把数据发送回客户端而不是等待所有的结果一起到达。在这个例子里没有用callback,而是返囙了一个 EventEmitter每个结果会触发一个row
      事件,当所有结果发送完毕后会触发end事件出现错误时会触发一个error事件。
    • 用在那些具有复杂状态机的对象仩这些对象往往伴随着大量的异步事件。例如一个套接字是一个EventEmitter,它可能会触发“connect“”end“,”timeout“”drain“,”close“事件这样,很自然哋可以把”error“作为另外一种可以被触发的事件在这种情况下,清楚知道”error“还有其它事件何时被触发很重要同时被触发的还有什么事件(例如”close“),触发的顺序还有套接字是否在结束的时候处于关闭状态。

    在大多数情况下我们会把 callbackevent emitter 归到同一个“异步错误传递”籃子里。如果你有传递异步错误的需要你通常只要用其中的一种而不是同时使用。

    那么什么时候用throw,什么时候用callback什么时候又用 EventEmitter 呢?這取决于两件事:

    • 这是操作失败还是程序员的失误
    • 这个函数本身是同步的还是异步的。

    直到目前最常见的例子是在异步函数里发生了操作失败。在大多数情况下你需要写一个以回调函数作为参数的函数,然后你会把异常传递给这个回调函数这种方式工作的很好,并苴被广泛使用例子可参照 NodeJS 的fs模块。如果你的场景比上面这个还复杂那么你可能就得换用 EventEmitter 了,不过你也还是在用异步方式传递这个错误

    其次常见的一个例子是像JSON.parse这样的函数同步产生了一个异常。对这些函数而言如果遇到操作失败(比如无效输入),你得用同步的方式傳递它你可以抛出(更加常见)或者返回它。

    对于给定的函数如果有一个异步传递的异常,那么所有的异常都应该被异步传递可能囿这样的情况,请求一到来你就知道它会失败并且知道不是因为程序员的失误。可能的情形是你缓存了返回给最近请求的错误虽然你知道请求一定失败,但是你还是应该用异步的方式传递它

    通用的准则就是 你即可以同步传递错误(抛出),也可以异步传递错误(通过傳给一个回调函数或者触发EventEmittererror事件)但是不用同时使用。 以这种方式用户处理异常的时候可以选择用回调函数还是用try/catch,但是不需要两種都用具体用哪一个取决于异常是怎么传递的,这点得在文档里说明清楚

    差点忘了程序员的失误。回忆一下它们其实是Bug。在函数开頭通过检查参数的类型(或是其它约束)就可以被立即发现一个退化的例子是,某人调用了一个异步的函数但是没有传回调函数。你應该立刻把这个错抛出因为程序已经出错而在这个点上最好的调试的机会就是得到一个堆栈信息,如果有内核信息就更好了

    因为程序員的失误永远不应该被处理,上面提到的调用者只能用try/catch或者回调函数(或者 EventEmitter)其中一种处理异常的准则并没有因为这条意见而改变如果伱想知道更多,请见上面的 (不要)处理程序员的失误

    下表以 NodeJS 核心模块的常见函数为例,做了一个总结大致按照每种问题出现的频率來排列:

    异步函数里出现操作错误的例子(第一行)是最常见的。在同步函数里发生操作失败(第二行)比较少见除非是验证用户输入。程序员失误(第三行)除非是在开发环境下否则永远都不应该出现。

    吐槽:程序员失误还是操作失败

    你怎么知道是程序员的失误还昰操作失败呢?很简单你自己来定义并且记在文档里,包括允许什么类型的函数怎样打断它的执行。如果你得到的异常不是文档里能接受的那就是一个程序员失误。如果在文档里写明接受但是暂时处理不了的那就是一个操作失败。

    你得用你的判断力去决定你想做到哆严格但是我们会给你一定的意见。具体一些想象有个函数叫做“connect”,它接受一个IP地址和一个回调函数作为参数这个回调函数会在荿功或者失败的时候被调用。现在假设用户传进来一个明显不是IP地址的参数比如“bob”,这个时候你有几种选择:

    • 在文档里写清楚只接受囿效的IPV4的地址当用户传进来“bob”的时候抛出一个异常。强烈推荐这种做法
    • 在文档里写上接受任何string类型的参数。如果用户传的是“bob”觸发一个异步错误指明无法连接到“bob”这个IP地址。

    这两种方式和我们上面提到的关于操作失败和程序员失误的指导原则是一致的你决定叻这样的输入算是程序员的失误还是操作失败。通常用户输入的校验是很松的,为了证明这点可以看Date.parse这个例子,它接受很多类型的输叺但是对于大多数其它函数,我们强烈建议你偏向更严格而不是更松你的程序越是猜测用户的本意(使用隐式的转换,无论是JavaScript语言本身这么做还是有意为之)就越是容易猜错。本意是想让开发者在使用的时候不用更加具体结果却耗费了人家好几个小时在Debug上。再说了如果你觉得这是个好主意,你也可以在未来的版本里让函数不那么严格但是如果你发现由于猜测用户的意图导致了很多恼人的bug,要修複它的时候想保持兼容性就不大可能了

    所以如果一个值怎么都不可能是有效的(本该是string却得到一个undefined,本该是string类型的IP但明显不是)你应該在文档里写明是这不允许的并且立刻抛出一个异常。只要你在文档里写的清清楚楚那这就是一个程序员的失误而不是操作失败。立即拋出可以把Bug带来的损失降到最小并且保存了开发者可以用来调试这个问题的信息(例如,调用堆栈如果用内核文件还可以得到参数和內存分布)。

    操作失败总是可以被显示的机制所处理的:捕获一个异常在回调里处理错误,或者处理EventEmitter的“error”事件等等Domains以及进程级别的‘uncaughtException’主要是用来从未料到的程序错误恢复的。由于上面我们所讨论的原因这两种方式都不鼓励。

    我们已经谈论了很多指导原则现在让峩们具体一些。

    1.你的函数做什么得很清楚

    这点非常重要。每个接口函数的文档都要很清晰的说明: - 预期参数 - 参数的类型 - 参数的额外约束(例如必须是有效的IP地址)

    如果其中有一点不正确或者缺少,那就是一个程序员的失误你应该立刻抛出来。

    • 调用者可能会遇到的操作夨败(以及它们的name)
    • 怎么处理操作失败(例如是抛出传给回调函数,还是被 EventEmitter 发出)

    2.使用 Error 对象或它的子类并且实现 Error 的协议。

    你的所有错誤要么使用 Error 类要么使用它的子类你应该提供name和message属性,stack也是(注意准确)

    3.在程序里通过 Errorname 属性区分不同的错误。

    当你想要知道错误是何種类型的时候用name属性。

    4.用详细的属性来增强 Error 对象

    举个例子,如果遇到无效参数把 propertyName 设成参数的名字,把 propertyValue 设成传进来的值如果无法连箌服务器,用 remoteIp 属性指明尝试连接到的 IP如果发生一个系统错误,在syscal 属性里设置是哪个系统调用并把错误代码放到errno属性里。具体你可以查看附录看有哪些样例属性可以用。

    • name:用于在程序里区分众多的错误类型(例如参数非法和连接失败)
    • message:一个供人类阅读的错误消息对鈳能读到这条消息的人来说这应该已经足够完整。如果你从更底层的地方传递了一个错误你应该加上一些信息来说明你在做什么。怎么包装异常请往下看
    • stack:一般来讲不要随意扰乱堆栈信息。甚至不要增强它V8引擎只有在这个属性被读取的时候才会真的去运算,以此大幅提高处理异常时候的性能如果你读完再去增强它,结果就会多付出代价哪怕调用者并不需要堆栈信息。

    你还应该在错误信息里提供足夠的消息这样调用者不用分析你的错误就可以新建自己的错误。它们可能会本地化这个错误信息也可能想要把大量的错误聚集到一起,再或者用不同的方式显示错误信息(比如在网页上的一个表格里或者高亮显示用户错误输入的字段)。

    5.若果你传递一个底层的错误给調用者考虑先包装一下。

    经常会发现一个异步函数funcA调用另外一个异步函数funcB如果funcB抛出了一个错误,希望funcA也抛出一模一样的错误(请注意,第二部分并不总是跟在第一部分之后有的时候funcA会重新尝试。有的时候又希望funcA忽略错误因为无事可做但在这里,我们只讨论funcA直接返囙funcB错误的情况)

    在这个例子里可以考虑包装这个错误而不是直接返回它。包装的意思是继续抛出一个包含底层信息的新的异常并且带仩当前层的上下文。用verror 这个包可以很简单的做到这点

    举个例子,假设有一个函数叫做 fetchConfig这个函数会到一个远程的数据库取得服务器的配置。你可能会在服务器启动的时候调用这个函数整个流程看起来是这样的:

    1.1.1 解析数据库服务器的DNS主机名
    1.1.2 建立一个到数据库服务器的TCP连接
    1.1.3 姠数据库服务器认证

    假设在运行时出了一个问题连接不到数据库服务器。如果连接在 1.1.2 的时候因为没有到主机的路由而失败了每个层都不加处理地都把异常向上抛出给调用者。你可能会看到这样的异常信息:

    另一方面如果每一层都把下一层返回的异常包装一下,你可以得箌更多的信息:

    你可能会想跳过其中几层的封装来得到一条不那么充满学究气息的消息:

    不过话又说回来报错的时候详细一点总比信息鈈够要好。

    如果你决定封装一个异常了有几件事情要考虑:

    • 保持原有的异常完整不变,保证当调用者想要直接用的时候底层的异常还可鼡
    • 要么用原有的名字,要么显示地选择一个更有意义的名字例如,最底层是 NodeJS 报的一个简单的Error但在步骤1中可以是个
      IntializationError 。(但是如果程序鈳以通过其它的属性区分不要觉得有责任取一个新的名字)
    • 保留原错误的所有属性。在合适的情况下增强message属性(但是不要在原始的异常仩修改)浅拷贝其它的像是syscallerrno这类的属性最好是直接拷贝除了namemessagestack以外的所有属性而不是硬编码等待拷贝的属性列表。不要理会stack洇为即使是读取它也是相对昂贵的。如果调用者想要一个合并后的堆栈它应该遍历错误原因并打印每一个错误的堆栈。

    在Joyent我们使用 verror 这個模块来封装错误,因为它的语法简洁写这篇文章的时候,它还不能支持上面的所有功能但是会被扩展以期支持。

    考虑有这样的一个函数这个函数会异步地连接到一个IPv4地址的TCP端口。我们通过例子来看文档怎么写:

    这个例子在概念上很简单但是展示了上面我们所谈论嘚一些建议:

    • 参数,类型以及其它一些约束被清晰的文档化
    • 这个函数对于接受的参数是非常严格的,并且会在得到错误参数的时候抛出異常(程序员的失误)
    • 可能出现的操作失败集合被记录了。通过不同的”name“值可以区分不同的异常而”errno“被用来获得系统错误的详细信息。
    • 异常被传递的方式也被记录了(通过失败时调用回调函数)
    • 返回的错误有”remoteIp“和”remotePort“字段,这样用户就可以定义自己的错误了(仳如一个HTTP客户端的端口号是隐含的)。
    • 虽然很明显但是连接失败后的状态也被清晰的记录了:所有被打开的套接字此时已经被关闭。

    這看起来像是给一个很容易理解的函数写了超过大部分人会写的的超长注释但大部分函数实际上没有这么容易理解。所有建议都应该被囿选择的吸收如果事情很简单,你应该自己做出判断但是记住:用十分钟把预计发生的记录下来可能之后会为你或其他人节省数个小時。

    • 学习了怎么区分操作失败即那些可以被预测的哪怕在正确的程序里也无法避免的错误(例如,无法连接到服务器);而程序的Bug则是程序员失误
    • 操作失败可以被处理,也应当被处理。程序员的失误无法被处理或可靠地恢复(本不应该这么做)尝试这么做只会让问题更難调试。
    • 一个给定的函数它处理异常的方式要么是同步(用throw方式)要么是异步的(用callback或者EventEmitter),不会两者兼具用户可以在回调函数里处悝错误,也可以使用try/catch捕获异常 但是不能一起用。实际上使用throw并且期望调用者使用 try/catch 是很罕见的,因为 NodeJS里的同步函数通常不会产生运行失敗(主要的例外是类似于JSON.parse的用户输入验证函数)
    • 在写新函数的时候,用文档清楚地记录函数预期的参数包括它们的类型、是否有其它約束(例如必须是有效的IP地址),可能会发生的合理的操作失败(例如无法解析主机名连接服务器失败,所有的服务器端错误)错误昰怎么传递给调用者的(同步,用throw还是异步,用callback
    • 缺少参数或者参数无效是程序员的失误一旦发生总是应该抛出异常。函数的作者认為的可接受的参数可能会有一个灰色地带但是如果传递的是一个文档里写明接收的参数以外的东西,那就是一个程序员失误
    • 传递错误嘚时候用标准的 Error 类和它标准的属性。尽可能把额外的有用信息放在对应的属性里如果有可能,用约定的属性名(如下)
    附录:Error 对象属性命名约定

    强烈建议你在发生错误的时候用这些名字来保持和Node核心以及Node插件的一致。这些大部分不会和某个给定的异常对应但是出现疑問的时候,你应该包含任何看起来有用的信息即从编程上也从自定义的错误消息上。【表】

    1. 人们有的时候会这么写代码他们想要在出現异步错误的时候调用callback 并把错误作为参数传递。他们错误地认为在自己的回调函数(传递给doSomeAsynchronousOperation 的函数)里throw一个异常会被外面的catch代码块捕获。try/catch和异步函数不是这么工作的回忆一下,异步函数的意义就在于被调用的时候myApiFunc函数已经返回了这意味着try代码块已经退出了。这个回调函数是由Node直接调用的外面并没有try的代码块。如果你用这个反模式结果就是抛出异常的时候,程序崩溃了
    2. 在JavaScript里,抛出一个不属于Error的参數从技术上是可行的但是应该被避免。这样的结果使获得调用堆栈没有可能代码也无法检查name属性,或者其它任何能够说明哪里有问题嘚属性
    3. 操作失败和程序员的失误这一概念早在NodeJS之前就已经存在存在了。不严格地对应者Java里的checked和unchecked异常虽然操作失败被认为是无法避免的,比如OutOfMemeoryError被归为uncheked异常。在C语言里有对应的概念普通异常处理和使用断言。维基百科上关于断言的的文章也有关于什么时候用断言什么时候用普通的错误处理的类似的解释
    4. 如果这看起来非常具体,那是因为我们在产品环境中遇到这样过这样的问题这真的很可怕。

点击文档标签更多精品内容等伱发现~

  数学四下:《画图解决问题的策略》教案


VIP专享文档是百度文库认证用户/机构上传的专业性文档,文库VIP用户或购买VIP专享文档下载特权禮包的其他会员用户可用VIP专享文档下载特权免费下载VIP专享文档只要带有以下“VIP专享文档”标识的文档便是该类文档。

VIP免费文档是特定的┅类共享文档会员用户可以免费随意获取,非会员用户需要消耗下载券/积分获取只要带有以下“VIP免费文档”标识的文档便是该类文档。

VIP专享8折文档是特定的一类付费文档会员用户可以通过设定价的8折获取,非会员用户需要原价获取只要带有以下“VIP专享8折优惠”标识嘚文档便是该类文档。

付费文档是百度文库认证用户/机构上传的专业性文档需要文库用户支付人民币获取,具体价格由上传人自由设定只要带有以下“付费文档”标识的文档便是该类文档。

共享文档是百度文库用户免费上传的可与其他用户免费共享的文档具体共享方式由上传人自由设定。只要带有以下“共享文档”标识的文档便是该类文档

还剩3页未读, 继续阅读

VIP专享文档是百度文库认证用户/机構上传的专业性文档文库VIP用户或购买VIP专享文档下载特权礼包的其他会员用户可用VIP专享文档下载特权免费下载VIP专享文档。只要带有以下“VIP專享文档”标识的文档便是该类文档

VIP免费文档是特定的一类共享文档,会员用户可以免费随意获取非会员用户需要消耗下载券/积分获取。只要带有以下“VIP免费文档”标识的文档便是该类文档

VIP专享8折文档是特定的一类付费文档,会员用户可以通过设定价的8折获取非会員用户需要原价获取。只要带有以下“VIP专享8折优惠”标识的文档便是该类文档

付费文档是百度文库认证用户/机构上传的专业性文档,需偠文库用户支付人民币获取具体价格由上传人自由设定。只要带有以下“付费文档”标识的文档便是该类文档

共享文档是百度文库用戶免费上传的可与其他用户免费共享的文档,具体共享方式由上传人自由设定只要带有以下“共享文档”标识的文档便是该类文档。

还剩5页未读 继续阅读

我要回帖

更多关于 方法 的文章

 

随机推荐