【问题标题】:JavaScript Callback Error HandlingJavaScript 回调错误处理
【发布时间】:2016-08-09 14:11:16
【问题描述】:

在函数中验证参数并返回错误是很常见的。

但是,在 JavaScript 回调函数中,如:

function myFunction(num, callback) {
  if (typeof num !== 'number') return callback(new Error('invalid num'))
  // do something else asynchronously and callback(null, result)
}

我写了很多这样的函数,但我想知道是否有潜在的危害。因为在大多数情况下,调用者假定这是一个异步函数,并且回调将在函数调用之后的代码之后执行。但是如果某些参数无效,该函数将立即调用回调。所以调用者必须小心处理这种情况,即意外的执行顺序。

我想听听关于这个问题的一些建议。我是否应该仔细假设所有异步回调都可以立即执行?或者我应该使用类似 setTimeout(..., 0) 的东西将同步的东西转换为异步的东西。或者有更好的解决方案我不知道。谢谢。

【问题讨论】:

    标签: javascript error-handling callback


    【解决方案1】:

    API 应该记录它会同步(如Array#sort)或异步(如Promise#then)调用回调,然后始终遵守记录在案的保证。它不应该混搭。

    所以是的,如果你有一个通常会异步调用回调的函数,它应该总是异步调用它,不管它为什么进行调用。

    在 jQuery 中有一个很好的例子:当 jQuery 首次添加“延迟”对象时,如果延迟已经解决,他们会同步调用回调,如果没有解决,则异步调用。这是许多混乱和错误的根源,这也是 ES2015 的承诺保证 then 和 catch 回调将始终异步调用的部分原因。


    如果可能并且与代码库的其余部分不冲突,请考虑使用Promises 而不是简单的回调。 Promise 为异步操作(以及与同步操作的互操作)提供了非常清晰、简单、有保证的语义和可组合性。

    【讨论】:

    • 我删除了我的反对票。我猜想对函数调用方式的验证非常严重,可以抛出并且无论如何都不应该在生产中发生。
    • @PatrickRoberts:我已经删除了答案的那一部分,它与问题并不真正相关。我明白你的意思,虽然我(还)不同意;我需要更多地考虑它。我注意到,如果你在设置 ES2015 Promise (let p = new Promise(resolve => { throw new Error(); })) 期间抛出,它会被 Promise 构造函数转换为拒绝,考虑到进入该 API 的设计...
    • 我的方法也是不正确的。正如评论者向我指出的那样,调用堆栈是一个非常重要的考虑因素,尤其是当使用您的函数的开发人员尝试无限期重试时,如果您的函数在出错时是同步的,最终会导致堆栈溢出。
    • Patrick - 是的,我确实认为异步异步和同步同步很重要。这与我删除的段落(以及您的原始答案)相矛盾。
    • 回过头来看,Promise 似乎已经走上了单一错误通道的道路,并且一直保持这种状态。不幸的是,这违背了我应该立即抛出 boneheaded exceptions 的理念,但由于 Promise 开始使用 async/await 修复混淆的堆栈跟踪,我想尽早验证和抛出已经不是那么重要了。
    【解决方案2】:

    异步函数的调用者应该知道调用该函数的结果。有一个异步函数应该返回什么的标准,Promises。

    如果您的函数返回Promise,任何人都可以轻松理解该函数中发生了什么。 Promise 有拒绝回调,但我们可以争论是否应该通过拒绝 Promise 来处理参数的验证,或者是否应该直接抛出异常。无论哪种方式,如果调用者使用catch 方法正确处理异常,则直接抛出的异常和拒绝都将以相同的方式捕获。

    function throwingFunction(num) {
      return new Promise(function (resolve, reject) {
    
        if (typeof num !== 'number') throw new Error('invalid num');
        // do something else asynchronously and callback(null, result)
      };
    }
    
    function rejectingFunction(num) {
      return new Promise(function (resolve, reject) {
    
        if (typeof num !== 'number') reject(new Error('invalid num'));
        // do something else asynchronously and callback(null, result)
      };
    }
    
    // Instead of passing the callback, create the promise and provide your callback to the `then` method.
    
    var resultThrowing = throwingFunction(num)
        .then(function (result) { console.log(result); })
        .catch(function (error) { console.log(error); });
    
    var resultRejecting = rejectingFunction(num)
        .then(function (result) { console.log(result); })
        .catch(function (error) { console.log(error); });
    

    这两种模式都会导致错误被捕获和记录。

    如果您使用 Promise,异步函数的调用者将不必担心您在函数内部的实现,您可以直接抛出错误,也可以随意拒绝 Promise。

    【讨论】:

      【解决方案3】:

      不,立即回调没有害处,实际上故意延迟错误只会浪费时间和开销。 是的,立即回调错误可能非常有害,应该避免一个假定为异步的函数! (看那个,180!)

      从开发人员的角度来看,设置只能在之后进行的原因有很多。例如here:

      const server = net.createServer(() => {}).listen(8080);
      
      server.on('listening', () => {});
      

      listening 事件直到调用 .listen(8080) 之后才会附加,因为事件源是从对 .listen() 的调用中返回的。在这种情况下,在.listen()执行完之后,再实现listening事件同步调用是不成功的。

      这是我想介绍的另一个案例:

      var num = '5';
      
      myFunction(num, function callback(err, result) {
        if (err) {
          return myFunction(num, callback);
        }
      
        // handle result
      });
      

      现在,如果您 callback 同步错误,此控制流将导致堆栈溢出。虽然这是开发人员的错,但是从一个预期为异步的函数中发生 stackoverflow 是一件非常糟糕的事情。这是使用setImmediate() 传递错误而不是立即执行callback 的优势之一。

      【讨论】:

        猜你喜欢
        • 2016-12-13
        • 2016-12-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-06-23
        • 2015-09-13
        • 2012-08-25
        相关资源
        最近更新 更多