使用async/await的try/catch块

230
我正在研究node 7的async/await特性,但一直遇到这样的代码。
function getQuote() {
  let quote = "Lorem ipsum dolor sit amet, consectetur adipiscing elit laborum.";
  return quote;
}

async function main() {
  try {
    var quote = await getQuote();
    console.log(quote);
  } catch (error) {
    console.error(error);
  }
}

main();

这似乎是使用async/await唯一可能的情况,即使用resolve/rejectreturn/throw,但是v8不会优化try/catch块内部的代码?!

有其他替代方案吗?


“await后抛出不成功”是什么意思?如果它出现错误?如果它没有返回预期的结果?你可以在catch块中重新抛出。 - DevDig
2
据我所知,V8会进行优化的try/catch语句块,而throw语句则是较慢的。 - Tamas Hegedus
1
我仍然不理解问题。你可以使用旧的 Promise 链接,但我认为这并不会更快。所以你担心 try-catch 的性能吗?那么这与 async/await 有什么关系呢? - Tamas Hegedus
请检查我的答案,我尝试找到更简洁的方法。 - zardilior
在这里你可以这样做 https://stackoverflow.com/a/61833084/6482248,看起来更加简洁。 - Prathamesh More
10个回答

266

替代方案

这是一个替代方案:

async function main() {
  try {
    var quote = await getQuote();
    console.log(quote);
  } catch (error) {
    console.error(error);
  }
}

使用显式的 Promises ,可能会像这样:

function main() {
  getQuote().then((quote) => {
    console.log(quote);
  }).catch((error) => {
    console.error(error);
  });
}

或者使用类似这样的方式,采用续传风格:

function main() {
  getQuote((error, quote) => {
    if (error) {
      console.error(error);
    } else {
      console.log(quote);
    }
  });
}

原始示例

你的原始代码所做的是挂起执行并等待由getQuote()返回的Promise解决。然后它继续执行并将返回的值写入var quote,如果promise已经被解决,则打印它;如果promise被拒绝,则抛出异常并运行catch块打印错误。

您可以直接使用Promise API来执行相同的操作,就像第二个示例中一样。

性能

现在,来测试一下性能吧!

我刚刚编写了这段代码 - f1()1作为返回值,f2()抛出1作为异常:

function f1() {
  return 1;
}

function f2() {
  throw 1;
}

现在让我们使用f1()重复调用相同的代码一百万次:

var sum = 0;
for (var i = 0; i < 1e6; i++) {
  try {
    sum += f1();
  } catch (e) {
    sum += e;
  }
}
console.log(sum);

接下来让我们把f1()变成f2()

var sum = 0;
for (var i = 0; i < 1e6; i++) {
  try {
    sum += f2();
  } catch (e) {
    sum += e;
  }
}
console.log(sum);

这是我对f1的测试结果:

$ time node throw-test.js 
1000000

real    0m0.073s
user    0m0.070s
sys     0m0.004s

这是我得到的 f2 内容:

$ time node throw-test.js 
1000000

real    0m0.632s
user    0m0.629s
sys     0m0.004s

看起来你可以在一个单线程的进程中做到每秒2百万次的操作。如果你需要做更多的话,那就要担心一下了。

概述

在Node中,我不会为这样的事情担心。如果这类问题经常被使用,那么V8、SpiderMonkey或Chakra团队最终会对其进行优化,并且大家都会跟进 - 这不是原则上没有优化,而是不是一个问题。

即使它没有被优化,我仍然认为,如果你在Node中占用了CPU的最大值,那么你可能应该用C语言编写你的数值计算 - 这就是本地插件的作用之一。或者也许像node.native这样的东西比Node.js更适合这项工作。

我想知道需要抛出这么多异常的用例是什么。通常,抛出异常而不是返回一个值,是一种特殊情况。


我知道这段代码可以很容易地使用 Promises 编写,正如之前提到的,在各种示例中都能看到。这也是我提出问题的原因。在 try/catch 中只有一个操作可能不是问题,但是如果有多个 async/await 函数并且还有进一步的应用逻辑,那就可能会出现问题。 - Patrick
6
“可能会”和“将会”之间存在着推测和实际测试的区别。我测试了单个语句,因为这是你问题中提到的内容,但你可以很容易地将我的例子转化为测试多个语句。我还提供了其他几种编写异步代码的选项,这也是你所问的。如果它回答了你的问题,那么你可以考虑接受这个答案。总之,异常当然比返回慢,但它们的使用应该是一个例外。 - rsp
2
抛出异常确实应该是一个例外。话虽如此,无论你是否抛出异常,代码都是未经优化的。性能损失来自于使用 try catch,而不是抛出异常。虽然数字很小,但根据你的测试,它几乎慢了10倍,这并不是微不足道的。 - Nepoxx

42

类似于Golang错误处理的备选方案

由于async/await在底层使用了promises,因此您可以编写一个小型实用程序函数,如下所示:

export function catchEm(promise) {
  return promise.then(data => [null, data])
    .catch(err => [err]);
}

每当您需要捕获一些错误时,请导入它,并使用它包装返回 Promise 的异步函数。

import catchEm from 'utility';

async performAsyncWork() {
  const [err, data] = await catchEm(asyncFunction(arg1, arg2));
  if (err) {
    // handle errors
  } else {
    // use data
  }
}

我创建了一个 NPM 包,它完全可以实现上述功能 - https://www.npmjs.com/package/@simmo/task - Mike
5
@Mike,你可能正在重新发明轮子 - 已经有一个流行的软件包可以完全实现这个功能:https://www.npmjs.com/package/await-to-js。 - Jakub Kukul
4
Golang 不是 Node。 - Jeremiah Adams
欢迎来到StackOverflow,在问题提出4年后,像"golang is not node"这样的回答浮现了出来。我认为重点是你可以在Node中编写一个实用函数来完成他所要求的功能。它可能是用Go编写的,但重点很明确。 - Dylan Wright
@DylanWright 这个答案甚至不是用Go写的,而是JavaScript。它只是在说明这是如何实现类似于Go的异步逻辑的。 - Levi Murray

31

一个替代try-catch代码块的方法是await-to-js库。我经常使用它。

import to from 'await-to-js';

async function main(callback) {
    const [err,quote] = await to(getQuote());
    
    if(err || !quote) return callback(new Error('No Quote found'));

    callback(null,quote);

}

与尝试捕获相比,此语法更加清晰简洁。


尝试过这个并且很喜欢。通过安装新模块来获得干净、易读的代码。但是如果你计划编写大量异步函数,我必须说这是一个很好的补充!谢谢。 - filipbarak
2
你甚至不需要安装库。如果你查看它的源代码,它只有一个函数。只需将该函数复制并粘贴到项目中的实用文件中,就可以使用了。 - iqbal125
这是一个 to 函数的单行代码:const to = promise => promise.then(res => [null, res]).catch(err => [err || true, null]); - Yeti
对于 iqbal125,使用库可以避免您承担文档、测试和 Typescript 支持的责任(只要该库经过测试、有文档并支持 ts)。 - Jean Claveau
iqbal125,使用一个库可以避免你承担文档、测试和Typescript支持的责任(只要这个库经过测试、有文档并支持ts)。 - undefined

24
async function main() {
  var getQuoteError
  var quote = await getQuote().catch(err => { getQuoteError = err }

  if (getQuoteError) return console.error(err)

  console.log(quote)
}

另一种方法是,而不是在顶部声明一个可能的变量来存储错误,你可以这样做:

或者,您可以不在顶部声明一个可能的变量来保存错误,而是进行如下操作:

Alternatively instead of declaring a possible var to hold an error at the top you can do

if (quote instanceof Error) {
  // ...
}

但如果像 TypeError 或 ReferenceError 这样的错误被抛出,那么这种方法是行不通的。但你可以通过以下方式确保抛出的是普通错误:

async function main() {
  var quote = await getQuote().catch(err => {
    console.error(err)      

    return new Error('Error getting quote')
  })

  if (quote instanceOf Error) return quote // get out of here or do whatever

  console.log(quote)
}

我倾向于将所有内容都包装在一个大的try-catch块中,当有多个承诺被创建时,这样做可能会使处理特定于创建它的承诺的错误变得繁琐。另一种选择是使用多个try-catch块,但我同样认为这种方式很繁琐。


21

一个更清晰的替代方案如下:

由于每个异步函数在技术上都是一个 Promise

在使用 await 调用函数时,你可以添加 catch 来处理错误

async function a(){
    let error;

    // log the error on the parent
    await b().catch((err)=>console.log('b.failed'))

    // change an error variable
    await c().catch((err)=>{error=true; console.log(err)})

    // return whatever you want
    return error ? d() : null;
}
a().catch(()=>console.log('main program failed'))

由于所有的 promise 错误都已被处理,且您没有代码错误,因此无需使用 try catch。在父级函数中可以省略它!

假设您正在使用 mongodb,在出现错误时,您可能更喜欢在调用该函数的函数中处理它,而不是制作包装器或使用 try-catch。


你有三个函数。一个用于获取值并捕获错误,另一个在没有错误时返回结果,最后一个是调用第一个函数并使用回调函数检查是否出现错误。所有这些都可以通过单个"promise".then(cb).catch(cb)或try-catch块来解决。 - Chief koshi
@Chiefkoshi,如您所见,单个catch无法胜任,因为所有三种情况都在处理错误。如果第一个失败了,它会返回d();如果第二个失败了,则返回null;如果最后一个失败了,则显示不同的错误消息。问题要求在使用await时处理错误。这也就是答案。即使任何一个失败,所有内容都应该执行。在这个特定的例子中,使用try catch块需要三个,这不是更简洁的方法。 - zardilior
2
问题并不要求在失败的 Promise 后执行。在这里,您等待 B,然后运行 C 并返回 D 如果它们出错了。这样做更干净吗?C 必须等待 B,但它们彼此独立。如果它们是相互依赖的,您将希望在 B 失败时停止执行 C,这是 .then.catch 或 try-catch 的工作。我假设它们不返回任何内容,并执行与 A 完全无关的一些异步操作。为什么要使用 async await 调用它们呢? - Chief koshi
这个问题涉及到在使用async/await时处理错误的替代方案。这里的示例仅用于描述,它展示了以顺序方式独立处理各个操作,这通常是async/await的使用方式。为什么称其为async await,只是为了展示如何处理。它更多的是描述性的,而不是合理化的。 - zardilior

4
我认为,一个简单而且解释得很好的例子来自于如何使用PromiseMDN DOCS
作为一个例子,他们使用了API Fetch的两种类型,一种是普通的,另一种是混合了异步和Promise的混合型
  1. 简单示例
async function myFetch() {
  let response = await fetch("coffee.jpg");
  // Added manually a validation and throws an error
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  let myBlob = await response.blob();

  let objectURL = URL.createObjectURL(myBlob);
  let image = document.createElement("img");
  image.src = objectURL;
  document.body.appendChild(image);
}

myFetch().catch((e) => {
  // Catches the errors...
  console.log("There has been a problem with your fetch operation: " + e.message);
});

  1. 混合方法

由于async关键字将函数转换为promise,因此您可以重构代码以使用混合的promise和await方法,将函数的后半部分提取到一个新块中,使其更加灵活:

async function myFetch() {
  // Uses async
  let response = await fetch("coffee.jpg");
  // Added manually a validation and throws an error
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return await response.blob();
}

myFetch()
  .then((blob) => {
    // uses plain promise
    let objectURL = URL.createObjectURL(blob);
    let image = document.createElement("img");
    image.src = objectURL;
    document.body.appendChild(image);
  })
  .catch((e) => console.log(e));

添加错误处理

  1. 正常
async function myFetch() {
  try {
    let response = await fetch("coffee.jpg");

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    let myBlob = await response.blob();
    let objectURL = URL.createObjectURL(myBlob);
    let image = document.createElement("img");
    image.src = objectURL;
    document.body.appendChild(image);
  } catch (e) {
    console.log(e);
  }
}

myFetch();

  1. 混合(最佳)
async function myFetch() {
  let response = await fetch("coffee.jpg");
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return await response.blob();
}

myFetch()
  .then((blob) => {
    let objectURL = URL.createObjectURL(blob);
    let image = document.createElement("img");
    image.src = objectURL;
    document.body.appendChild(image);
  })
  .catch(
    (
      e // Not need a try catch. This will catch it all already!
    ) => console.log(e)
  );

最佳解决方案

给出的最佳解决方案遵循这些原则,但更加明确的是这个答案 -> StackOverflow: try/catch blocks with async/await。我相信这里是。

function promiseHandle(promise) {
  return promise.then((data) => [null, data]).catch((err) => [err]);
}

async function asyncFunc(param1, param2) {
  const [err, data] = await promiseHandle(expensiveFunction(param1, param2));
  // This just to show, that in this way we can control what is going on..
  if (err || !data) {
    if (err) return Promise.reject(`Error but not data..`);
    return Promise.reject(`Error but not data..`);
  }
  return Promise.resolve(data);
}


我认为你忘记了,在最后一个代码块中,最后的Promise.reject应该是拒绝带有数据的? - Neithan Max

3

不需要像 await-to-js 这样的库,一个简单的一行代码就可以完成 to 函数(也显示在其他答案中):

const to = promise => promise.then(res => [null, res]).catch(err => [err || true, null]);

使用方法:

async function main()
{
    var [err, quote] = await to(getQuote());
    if(err)
    {
        console.log('warn: Could not get quote.');
    }
    else
    {
        console.log(quote);
    }
}

然而,如果错误导致函数或程序终止,例如:
async function main()
{
    var [err, quote] = await to(getQuote());
    if(err) return console.error(err);
    console.log(quote);
}

如果是这样的话,你可以让错误自动从main()返回,这也是异常的预期目的:

async function main()
{
    var quote = await getQuote();
    console.log(quote);
}

main().catch(err => console.error('error in main():', err));

抛出错误与返回错误

如果你预计会遇到一个错误,那么使用 throwreject 是不好的实践。相反,让 getQuote() 函数总是使用以下任何一种方式解决:

  • resolve([err, result])
  • resolve(null)
  • resolve(new Error(...))
  • resolve({error: new Error(), result: null})
  • 等等。

抛出错误(或在异步情况下等价于拒绝一个 Promise)必须保持为例外情况。由于异常只会在事情变糟时发生,并且不应该在正常使用期间发生,因此优化不是首要任务。因此,异常的唯一后果可能是函数的终止,这是默认行为,如果未被捕获的话。

除非你处理设计不良的第三方库,或者你正在为意外用例使用第三方库函数,否则你可能不应该使用 to 函数。


2
我希望采用这种方式:)
const sthError = () => Promise.reject('sth error');

const test = opts => {
  return (async () => {

    // do sth
    await sthError();
    return 'ok';

  })().catch(err => {
    console.error(err); // error will be catched there 
  });
};

test().then(ret => {
  console.log(ret);
});

这类似于使用co处理错误。

const test = opts => {
  return co(function*() {

    // do sth
    yield sthError();
    return 'ok';

  }).catch(err => {
    console.error(err);
  });
};

代码不是很清晰啊,看起来挺有意思的,你能修改一下吗? - zardilior
很遗憾,这个答案中没有解释,因为它实际上展示了一种避免在每个使用await的常量赋值时都需要使用try-catch的好方法! - Jim

1

对于Express框架,我通常遵循以下方法。我们可以创建一个解决Promise的函数。例如catchAsync函数:

const catchAsync = (fn) => (req, res, next) =>{
    Promise.resolve(fn(req, res, next)).catch((err) => next(err));
});

这个函数可以在需要try/catch的任何地方调用。它接受我们要调用的函数,并根据正在执行的操作解析或拒绝它。以下是我们如何调用它:

const sampleFunction = catchAsync(async (req, res) => {
           const awaitedResponse = await getResponse();
           res.send(awaitedResponse);
});

1
以这种方式进行捕获,在我的经验中是危险的。整个堆栈中抛出的任何错误都将被捕获,而不仅仅是来自此 promise 的错误(这可能不是您想要的)。
承诺的第二个参数已经是一个拒绝/失败回调。最好和更安全的是使用它。
这里是我编写的一个 TypeScript 类型安全的一行代码来处理这个问题:
function wait<R, E>(promise: Promise<R>): [R | null, E | null] {
  return (promise.then((data: R) => [data, null], (err: E) => [null, err]) as any) as [R, E];
}

// Usage
const [currUser, currUserError] = await wait<GetCurrentUser_user, GetCurrentUser_errors>(
  apiClient.getCurrentUser()
);

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