JavaScript回调错误处理

10

在函数中验证参数并返回错误是一种常见的做法。

然而,在 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)的东西来将同步事物转换为异步事物。或者有一个更好的解决方案我不知道。谢谢。

3个回答

5
一个API应该明确文档化它将调用回调函数 要么 同步地(像Array#sort),要么异步地(像Promise#then),然后 始终遵守该文档化的保证。它不应该混合使用。
所以,是的,如果你有一个通常会异步调用回调函数的函数,它应该 始终 异步调用它,无论为什么进行调用。
在jQuery中有一个很好的例子:当jQuery首次添加“deferred”对象时,如果已经解决了deferred,则会同步调用回调函数,但如果还没有解决,则会异步调用。这是许多混淆和错误的根源,这也是为什么ES2015的promises保证thencatch回调将始终异步调用的部分原因。
如果可能且不与代码库的其余部分相冲突,请考虑使用Promises而不是简单的回调函数。Promises为异步操作(以及与同步操作的互操作性)提供了非常清晰、简单、可靠的语义和组合性。

我取消了我的踩票。我想函数调用的验证足够严格,会抛出异常,在生产环境中不应该发生。 - Patrick Roberts
@PatrickRoberts:我已经删除了答案的那部分内容,它与问题并不相关。我能理解你的观点,尽管我还不完全同意;我需要更多地思考。我注意到,如果在 ES2015 Promise 的设置过程中抛出异常(let p = new Promise(resolve => { throw new Error(); })),它会被 Promise 构造函数转换为拒绝,这支持了你关于单一错误通道的论点,因为该 API 的设计经历了深思熟虑和实践… - T.J. Crowder
我的方法也是不正确的。正如评论者指出的那样,调用堆栈是需要考虑的非常重要的因素,特别是当使用你的函数的开发人员尝试无限次重试时,如果你的函数在错误时是同步的,最终会导致堆栈溢出。 - Patrick Roberts
1
Patrick - 是的,我认为异步应该是异步,同步应该是同步。这与我删除的段落(和你最初的回答)相悖。 - T.J. Crowder
回顾一下,Promises似乎已经成为单个错误通道的代表,并且将一直保持这种方式。不幸的是,这与我的哲学相违背,即应立即抛出“愚蠢异常”,但由于Promises开始通过async/await修复混淆的堆栈跟踪,所以尽早验证和抛出异常可能不再是那么重要了。 - Patrick Roberts
显示剩余2条评论

2

你的异步函数的调用者应该知道调用函数后会得到什么结果。有一个标准规定了异步函数应该返回什么,那就是 Promise。

如果你的函数返回一个 Promise,任何人都可以轻松理解这个函数在做什么。Promise 有 reject 回调函数,但我们可以讨论一下参数验证是否应该通过拒绝 Promise 来处理,或者直接抛出异常。无论哪种方式,如果调用者使用 catch 方法正确地处理异常,直接抛出的异常和拒绝的 Promise 都将以相同的方式被捕获。

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。


1
不,立即回调并不会有害,事实上故意延迟错误只会浪费时间和开销。对于被假定为异步的函数,立即回调错误可能非常有害,应该避免!(看看这个,180度大转弯!)
从开发者的角度来看,为什么设置只能在之后完成有多个好理由。例如这里
const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});

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

以下是另一个我想要提出的案例:

var num = '5';

myFunction(num, function callback(err, result) {
  if (err) {
    return myFunction(num, callback);
  }

  // handle result
});

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接