JavaScript Promises - 拒绝与抛出

IT技术 javascript promise
2021-01-28 15:30:12

我已经阅读了几篇关于这个主题的文章,但我仍然不清楚Promise.reject与抛出错误之间是否存在差异例如,

使用 Promise.reject

return asyncIsPermitted()
    .then(function(result) {
        if (result === true) {
            return true;
        }
        else {
            return Promise.reject(new PermissionDenied());
        }
    });

使用投掷

return asyncIsPermitted()
    .then(function(result) {
        if (result === true) {
            return true;
        }
        else {
            throw new PermissionDenied();
        }
    });

我的偏好throw只是因为它更短而使用,但想知道是否有任何优势。

6个回答

使用一个和另一个没有任何优势,但是,有一种特定的情况是throw行不通的。但是,这些情况可以修复。

任何时候在 promise 回调中,都可以使用throw. 但是,如果您处于任何其他异步回调中,则必须使用reject.

例如,这不会触发捕获:

new Promise(function() {
  setTimeout(function() {
    throw 'or nah';
    // return Promise.reject('or nah'); also won't work
  }, 1000);
}).catch(function(e) {
  console.log(e); // doesn't happen
});

相反,您会留下一个未解决的Promise和一个未捕获的异常。在这种情况下,您希望改为使用reject. 但是,您可以通过两种方式解决此问题。

  1. 通过在超时内使用原始 Promise 的拒绝函数:

new Promise(function(resolve, reject) {
  setTimeout(function() {
    reject('or nah');
  }, 1000);
}).catch(function(e) {
  console.log(e); // works!
});

  1. 通过Promise超时:

function timeout(duration) { // Thanks joews
  return new Promise(function(resolve) {
    setTimeout(resolve, duration);
  });
}

timeout(1000).then(function() {
  throw 'worky!';
  // return Promise.reject('worky'); also works
}).catch(function(e) {
  console.log(e); // 'worky!'
});

值得一提的是,您不能使用的非Promise异步回调中的地方throw error,您也不能使用return Promise.reject(err)OP 要求我们进行比较的地方。这基本上就是为什么你不应该将异步回调放在 Promise 中的原因。Promise所有异步的东西,然后你就没有这些限制。
2021-03-13 15:30:12
@KevinB 我的意思是,对于任何片段,无论您使用的是throw还是Promise.reject,您都会得到完全相同的行为。例如,没有捕获错误的代码段 1 将不会捕获它,无论您是否使用throw 'or nah'return Promise.reject('or nah')
2021-03-21 15:30:12
@KevinB 阅读这些行“在特定情况下 throw 不起作用。” 和“任何时候你在Promise回调中,你都可以使用 throw。但是,如果你在任何其他异步回调中,你必须使用拒绝。” 我有一种感觉,示例片段将显示throw不起作用的情况,而是Promise.reject更好的选择。然而,这些片段不受这两个选择中的任何一个影响,并且无论您选择什么,都会给出相同的结果。我错过了什么吗?
2021-04-03 15:30:12
“但是,如果您处于任何其他类型的回调中”实际上应该是“但是,如果您处于任何其他类型的异步回调中”。回调可以是同步的(例如 with Array#forEach),并且对于那些,将它们放入其中会起作用。
2021-04-04 15:30:12
是的。如果在 setTimeout 中使用 throw,则不会调用 catch。您必须使用reject传递给new Promise(fn)回调的 。
2021-04-05 15:30:12

另一个重要的事实是,reject() 并不像一个终止控制流return语句一样。相反,throw确实会终止控制流。

例子:

new Promise((resolve, reject) => {
  throw "err";
  console.log("NEVER REACHED");
})
.then(() => console.log("RESOLVED"))
.catch(() => console.log("REJECTED"));

对比

new Promise((resolve, reject) => {
  reject(); // resolve() behaves similarly
  console.log("ALWAYS REACHED"); // "REJECTED" will print AFTER this
})
.then(() => console.log("RESOLVED"))
.catch(() => console.log("REJECTED"));

嗯,这一点是正确的,但比较是棘手的。因为通常你应该通过写入返回被拒绝的Promisereturn reject(),所以下一行不会运行。
2021-03-21 15:30:12
这让我绊倒了一段时间。是否有任何reject()不终止流程的充分理由好像应该。
2021-03-22 15:30:12
为什么要退货?
2021-04-02 15:30:12
在这种情况下,return reject()只是一种简写,reject(); return即您想要的是终止流。在返回值执行(函数传递给new Promise)没有使用,所以这是安全的。
2021-04-08 15:30:12
@223seneca reject 只是一个普通的 javascript 函数,就像其他任何函数一样,因此它不能终止流,因为函数通常不能终止它们的调用者。
2021-04-09 15:30:12

是的,最大的区别是拒绝是在Promise被拒绝后执行的回调函数,而throw不能异步使用。如果您选择使用拒绝,您的代码将继续以异步方式正常运行,而throw将优先完成解析器功能(此功能将立即运行)。

我看到的一个帮助我澄清问题的示例是,您可以使用拒绝设置超时函数,例如:

new Promise((resolve, reject) => {
  setTimeout(()=>{reject('err msg');console.log('finished')}, 1000);
  return resolve('ret val')
})
.then((o) => console.log("RESOLVED", o))
.catch((o) => console.log("REJECTED", o));

上面不可能用 throw 写。

try{
  new Promise((resolve, reject) => {
    setTimeout(()=>{throw new Error('err msg')}, 1000);
    return resolve('ret val')
  })
  .then((o) => console.log("RESOLVED", o))
  .catch((o) => console.log("REJECTED", o));
}catch(o){
  console.log("IGNORED", o)
}

在 OP 的小示例中,无法区分的差异但是在处理更复杂的异步概念时,两者之间的差异可能很大。

@DavidSpector - 不,我对 promise 非常熟悉,而且我也在努力理解上面解释的内容。:-) 除非它是在谈论Kevin B在上述之后发布的同一件事当然,关于“优先考虑”某事的内容尚不清楚。金发女郎,你想澄清一下吗?
2021-03-25 15:30:12
这听起来像一个关键概念,但我不理解它的书面意思。我猜对 Promises 来说还是太新了。
2021-04-04 15:30:12
这是不正确的。throw new Error("o_O") 与 reject(new Error("o_O")) 相同。参考learn-javascript-ru.translate.goog/...
2021-04-10 15:30:12

TLDR: 当函数有时返回Promise有时抛出异常时,它很难使用。编写异步函数时,更喜欢通过返回被拒绝的Promise来表示失败

您的特定示例混淆了它们之间的一些重要区别:

因为您Promise链中进行错误处理,所以抛出的异常会自动转换为被拒绝的Promise。这可以解释为什么它们似乎可以互换——它们不是。

考虑以下情况:

checkCredentials = () => {
    let idToken = localStorage.getItem('some token');
    if ( idToken ) {
      return fetch(`https://someValidateEndpoint`, {
        headers: {
          Authorization: `Bearer ${idToken}`
        }
      })
    } else {
      throw new Error('No Token Found In Local Storage')
    }
  }

这将是一种反模式,因为您将需要同时支持异步和同步错误情况。它可能看起来像:

try {
  function onFulfilled() { ... do the rest of your logic }
  function onRejected() { // handle async failure - like network timeout }
  checkCredentials(x).then(onFulfilled, onRejected);
} catch (e) {
  // Error('No Token Found In Local Storage')
  // handle synchronous failure
} 

不好,这正是Promise.reject(在全局范围内可用)可以解决问题并有效地将其与throw. 重构现在变成:

checkCredentials = () => {
  let idToken = localStorage.getItem('some_token');
  if (!idToken) {
    return Promise.reject('No Token Found In Local Storage')
  }
  return fetch(`https://someValidateEndpoint`, {
    headers: {
      Authorization: `Bearer ${idToken}`
    }
  })
}

这现在允许您仅使用一个catch()来处理网络故障缺少令牌的同步错误检查:

checkCredentials()
      .catch((error) => if ( error == 'No Token' ) {
      // do no token modal
      } else if ( error === 400 ) {
      // do not authorized modal. etc.
      }
是的,你是对的 @chenop - 为了捕捉那些意外的错误,你需要仍然在 try/catch 中包装
2021-03-14 15:30:12
很好的答案,但我在这里发现了一个缺陷——这种模式假设所有错误都是通过返回 Promise.reject 来处理的——所有可能从 checkCredentials() 抛出的意外错误会发生什么?
2021-03-18 15:30:12
我不明白@maxwell 的情况。难道你不能只是构造它,这样你就checkCredentials(x).then(onFulfilled).catch(e) {}可以catch处理拒绝情况和抛出的错误情况吗?
2021-03-24 15:30:12
然而,Op 的例子总是返回一个Promise。问题是指您是否应该使用Promise.rejectthrow何时想要返回被拒绝的Promise(将跳转到下一个的Promise.catch())。
2021-03-28 15:30:12
@maxwell - 我喜欢你的例子。同时,如果在 fetch 中添加一个 catch 并在其中抛出异常,那么您可以安全地使用 try ... catch ... 异常流没有完美的世界,但我认为使用一个单一模式是有道理的,组合模式是不安全的(与你的模式与反模式的类比一致)。
2021-04-08 15:30:12

有一个区别 - 这应该无关紧要 - 其他答案没有涉及,所以:

如果执行处理程序传递给thenthrows,则该调用返回的Promise将then被抛出的内容拒绝。

如果它返回一个被拒绝的Promise,调用返回的Promise将then解析为该Promise(并且最终将被拒绝,因为它被解析为的Promise被拒绝),这可能会引入一个额外的异步“滴答”(在微任务队列,用浏览器术语来说)。

但是,任何依赖于这种差异的代码都从根本上被破坏了。:-) 它不应该对Promise结算的时间那么敏感。

下面是一个例子:

如果你运行它,在撰写本文时,你会得到:

使用抛出错误:2 不是 42!
usingReject 错误:1 不是 42!

注意顺序。

将其与相同的链进行比较,但都使用usingThrow

这表明拒绝处理程序以其他顺序运行:

使用抛出错误:1 不是 42!
使用抛出错误:2 不是 42!

我在上面说“可能”是因为在其他领域有一些工作在其他类似情况下删除了这种不必要的额外勾选,如果涉及的所有Promise都是本机Promise(不仅仅是 thenables)。(特别是:在一个async函数中,return await x最初引入了一个额外的异步滴答,return x而其他方面是相同的;ES2020 对其进行了更改,以便如果x是本机Promise,则在没有其他区别的情况下删除额外的滴答。)

同样,任何对Promise的结算时间如此敏感的代码都已经被破坏了所以真的不重要/不应该重要。

实际上,正如其他答案所提到的:

  • 正如凯文 B 指出的那样throw如果您正在回调到您在履行处理程序中使用的某个其他函数则将无法工作——这是最大的问题
  • 正如lukyer 指出的那样throw突然终止该函数,这可能很有用(但您return在示例中使用的是相同的功能)
  • 正如Vencator 所指出的,您不能throw在条件表达式 ( ? :) 中使用,至少现在不能

除此之外,这主要是风格/偏好问题,因此与大多数风格/偏好一样,与您的团队达成一致(或者您不关心任何一种方式),并保持一致。