||

如何正确处理Promise的错误和异常

如果对 Promisetry catch 理解不够,很多时候会出现一个 Promise 中出现的错误无法被捕获的情况,这种情况在开发中相对来说比较常见,本文主要介绍如果处理异步错误,比如 Promise 对象中抛出的错误。

try catch

首先需要明白,try catch 只能处理当前上下文中抛出的错误,换句话说就是只能处理同步错误,所以下面的代码是很好理解的:

try {
  throw 'this is an error message';
} catch (e) {
  console.log('error caught');
}

在这个例子中,控制台可以很容易的打印出 error caught 字符串。但对于异步抛出的错误,try catch 可能就无法正常处理,比如下面的代码:

try {
  setTimeout(() => {
    throw 'this is an error message';
  }, 0);
} catch (e) {
  console.log('error caught');
}

亦或者这样:

try {
  Promise.reject('this is an error message');
} catch (e) {
  console.log('error caught');
}

上面的代码都无法正常捕获到错误,要解释这一点,还是需要回到开头,try catch 捕获的永远是同步的错误。

什么是同步的错误?

当在一个事件循环内,同一个任务队列中出现的错误,对于这个任务所在的上下文而言,就是同步错误。

setTimeoutPromise 被称为任务源,来自不同的任务源注册的回调函数会被放入到不同的任务队列中。

第一次事件循环中,JavaScript 引擎会把整个 script 代码当成一个宏任务执行,执行完成之后,再检测本次循环中是否寻在微任务,存在的话就依次从微任务的任务队列中读取执行完所有的微任务,再读取宏任务的任务队列中的任务执行,再执行所有的微任务,如此循环。JS 的执行顺序就是每次事件循环中的 宏任务–微任务 的不断切换。

再看 setTimeout 中抛出的错误,这个错误已经不在 try catch 所在的时间循环中了,所以这是一个异步错误,无法被 try catch 捕获到。

同理,Promise.reject() 此处虽然是同步执行的,但是此处 reject 的内容却在另一个微任务循环中,对于 try catch 来讲也不是同步的,所以这两个错误都无法被捕获。

Promise.reject

要理解 Promise.reject 首先要了解它的返回值,Promise.reject 返回的是一个 Promise 对象,请注意,是 Promise 对象。

Promise 对象在任何时候都是一个合法的对象,它不是错误也不是异常,所以在任何实现,直接对 Promise.reject 或者一个返回 Promise 对象的调用直接 try catch 是没有意义的,一个正常的对象永远不可能触发 catch 捕获。

所以,下面的代码中的 try catch 是无意义的:

function getSomeData() {
  Promise.reject('this is an error message');
}

function run() {
  try {
    getSomeData();
  } catch (e) {
    console.log('error caught');
  }
}

run();

可能会有人感觉到疑惑,明明调用 getSomeData 方法时,Promise 已经通过 reject 抛出了错误。为什么 try catch 捕获不到?

首先,需要知道,对于一个函数的错误是否可以被捕获到,可以尝试将函数调用的返回值替换到函数调用处,看看是否为一个错误。在上面的例子中,当然是替换 getSomeData() 这个调用了。

那么 getSomeData() 调用会被替换成什么?当然是 undefined 了。

对于一个没有明确 return 的函数调用,其返回值永远是 undefined 的,所以上面的 try catch 部分就变成了:

try {
  undefined;
} catch (e) {
  console.log('error caught');
}

那么这算是异常吗?了解 JS 基础的人肯定知道,这不算异常,这个代码会正常执行,不会走到 catch 中,所以在这个示例中,Promise.reject 抛出的异常无法被捕获。

可能会有另一种思路,就是将 Promise.reject 返回出去,那么代码就变成:

function getSomeData() {
  return Promise.reject('this is an error message');
}

function run() {
  try {
    getSomeData();
  } catch (e) {
    console.log('error caught');
  }
}

run();

这里的语义上是将 Promise.reject 异常返回到调用处,然而这个依然是无效的。Promise.reject 返回的是一个 Promise 对象,它是对象,不是错误。所以在 try catch 中完成 getSomeData() 调用后这里会出现一个 Promise 对象,这是一个在正常不过的对象了,不会被 catch 捕获,所以这个 try catch 依然是无效的。

于是,可能又出现了另一种思路:在调用处使用 Promise 的 catch 方法进行捕获,于是代码变成了:

function getSomeData() {
  return Promise.reject('this is an error message');
}

function run() {
  try {
    getSomeData().catch(console.log)
  } catch (e) {
    console.log('error caught');
  }
}

run();

这种是可行的,reject 的错误可以被捕获,但这不是 try catch 的功劳,而是 Promise 的内部消化,所以这里的 try catch 依然没有意义。

解决 Promise 异常捕获

Promise 异常是最常见的异步异常,其内部的错误基本都是被包装成了 Promise 对象后进行传递,所以解决 Promise 异常捕获整体思路有两个:

  • 使用 Promisecatch 方法内部消化;
  • 使用 asyncawait 将异步错误转同步错误再由 try catch 捕获;

下面介绍这两种处理思路。

Promise.catch

对于 Promise.reject 中抛出的错误,或者 Promise 构造器中抛出的错误,亦或者 then 中出现的错误,无论是运行时还是通过 throw 主动抛出的,原则上都可以被 catch 所捕获。

比如使用下面的方法:

function getSomeData() {
  Promise.reject('this is an error message').catch(console.log);
}

function run() {
  getSomeData();
}

run();

亦或者在调用处捕获,但这需要被调用的函数能返回 Promise 对象:

function getSomeData() {
  return Promise.reject('this is an error message');
}

function run() {
  getSomeData().catch(console.log);
}

run();

上面的两个方案都可行,事实上建议在业务逻辑允许的情况下,将 Promise 都返回出去,以便能向上传递,同时配合 unhandledrejection 进行兜底。

async await 异步转同步

使用 async 和 await 可以将一个异步函数调用在语义上变成同步执行的效果,这样我们就可以使用 try catch 去统一处理了。

对于上面的示例,可以这样修改:

function getSomeData() {
  return Promise.reject('this is an error message');
}

async function run() {
  try {
    await getSomeData();
  } catch (e) {
    console.log(e);
  }
}

run();

但需要注意,如果 getSomeData 方法没有写 return ,那么就无法将 Promise 对象向上传递,那么调用处的 await 等到的就是一个展开的 undefined ,其效果与文章开头的代码是类似,即:

function getSomeData() {
  Promise.reject('this is an error message');
}

async function run() {
  try {
    await getSomeData();
  } catch (e) {
    console.log(e);
  }
}

run();

上面的代码无法通过 try catch 捕获错误,因为没有将 Promise 向上传递,以至于 await 等到并展开的结果并非异常,无法触发 catch 捕获。

注意事项

一个函数如果内部处理了 Promise 异步对象,那么原则上其处理结果应该也是一个 Promise 对象,对于需要进行错误捕获的场景,Promise 对象应该始终通过 return 向上传递。

兜底方案

一般情况下,同步错误如果没有进行捕获,那么这个错误所在的时间循环将终止,所以对于在开发阶段没有捕获的错误,使用一种方法进行兜底是很有必要的。

对于同步错误,可以定义 window.onerror 进行兜底处理,或者使用 window.addEventListener(‘error’, errorHandler) 来定义兜底函数。

对于 Promise 异常,则可以同步使用 window.onunhandledrejection 或者 window.addEventListener(‘unhandledrejection’, errorHandler) 来定义兜底函数。

代码就不写了。

类似文章