try/catch 块未捕获 async/await 错误

try/catch block not catching async/await error

我有一个简单的 timeout 函数,它包装了一个带有超时的异步函数,以确保它在预设时间后失败。超时函数如下:

export default async function<T = any>(
  fn: Promise<T>,
  ms: number,
  identifier: string = ""
): Promise<T> {
  let completed = false;
  const timer = setTimeout(() => {
    if (completed === false) {
      const e = new Error(`Timed out after ${ms}ms [ ${identifier} ]`);
      e.name = "TimeoutError";
      throw e;
    }
  }, ms);
  const results = await fn;
  completed = true;
timer.unref();

  return results;
}

然后我在这个简单的代码片段中使用这个函数来确保 fetch 请求(使用 node-fetch 实现)被转换为文本输出:

let htmlContent: string;
  try {
    htmlContent = await timeout<string>(
      response.text(),
      3000,
      `waiting for text conversion of network payload`
    );
  } catch (e) {
    console.log(
      chalk.grey(`- Network response couldn\'t be converted to text: ${e.message}`)
    );
    problemsCaching.push(business);
    return business;
  }

当 运行 此代码经过多次迭代时,大多数 URL 端点提供了一个可以轻松转换为文本的有效负载,但偶尔会出现一个似乎只是挂起 fetch 调用。在这些情况下,超时实际上会触发,但抛出的 TimeoutError 不会被 catch 块捕获,而是会终止 运行 程序。

我有点莫名其妙。我现在确实经常使用 async/await,但在我的理解中可能仍有一些粗糙的边缘。谁能解释一下我如何有效地捕获并处理这个错误?

抛出的错误只有在 直接包含的函数 有某种错误处理时才会被捕获。传递给 setTimeout 的匿名函数不是 async 函数本身,因此如果 separate [=15],async 函数不会停止执行=] 一段时间后抛出:

const makeProm = () => new Promise(res => setTimeout(res, 200));
(async () => {
  setTimeout(() => {
    throw new Error();
  }, 200);
  await makeProm();
  console.log('done');
})()
  .catch((e) => {
    console.log('caught');
  });

这看起来是使用 Promise.race 的好时机:将 fetch Promise 传递给它,同时将 Promise 在传递 [=] 之后拒绝20=]参数:

async function timeout(prom, ms) {
  return Promise.race([
    prom,
    new Promise((res, rej) => setTimeout(() => rej('timeout!'), ms))
  ])
}

(async () => {
  try {
    await timeout(
      new Promise(res => setTimeout(res, 2000)),
      500
    )
   } catch(e) {
      console.log('err ' + e);
   }
})();

此错误发生在单独的调用堆栈中,因为它是从回调中抛出的。它与 try / catch 块内的同步执行流程完全分开。

您想在超时或成功回调中操作同一个 promise 对象。这样的东西应该会更好:

return new Promise( ( resolve, reject ) => {
    let rejected = false;
    const timer = setTimeout( () => {
        rejected = true;
        reject( new Error( 'Timed out' ) );
    }, ms ).unref();
    fn.then( result => {
        clearTimeout( timer );
        if ( ! rejected ) {
            resolve( result ) );
        }
    } );
} );

如果没有 rejectedclearTimeout,它可能也能正常工作,但这样您可以确保调用 resolvereject,而不是同时调用两者。

您会注意到我在这里的任何地方都没有使用 awaitthrow!如果您在使用异步代码时遇到问题,最好先使用单一样式编写它(所有回调,或所有承诺,或所有 "synchronous" 样式都使用 await)。

这个例子特别不能只用await来写,因为你需要同时有两个任务运行(超时和请求)。您可能会使用 Promise.race(),但您仍然需要 Promise 才能使用。

我会提供一些一般规则,因为答案已经给出。

Try/catch is by default synchronous. That means that if an asynchronous function throws an error in a synchronous try/catch block, no error throws.