在方法内使用 promise ,罕见的,可能导致抛出的 error 数量与预期不符合。这篇文章旨在构造这样的特殊案例,并尝试对 promise 抛异常的原理进行分析,而非介绍如何捕获异常。
如果我的理解存在偏差,欢迎讨论或勘误
(Tim: 911120823)
控制台显示 error(in promise) 的原因
尽管处理 error(in promise) 的方式,与普通的 error 大相径庭,promise 内抛出的错误,无法被常规的 window.onerror 等方式捕获。但既然是异常,由异常上抛机制逐层传递,必然与 object 之类的东西无关(看起来是废话,但把 'object' 替换成 promise 就能发现,异常数量不符合预期,是因为我的下意识认为,其数量与 promise 数量直接相关
)。
Promise.resolve 与 Promise.reject
Promise.resolve(value) ,如果 value 是 promise,value 充当返回值(即,不会生成新的异步操作步骤或链
)。否则返回成功状态的 promise。
Promise.reject(reason) ,参数只是描述失败的原因。表示异步操作链执行失败,隐式的在异步操作链中
(非同步)上抛异常;
案例及分析
方法返回 rejected promise
// 异常数量:1
const rejectedP = Promise.reject('-');
const fun = p => p;
rejectedP.finally(fun(rejectedP));
显而易见,调用 Promise.reject 会在异步操作链中上抛异常,但是方法返回 rejected promise,并不会产生新 error
。
一个 rejected promise 派生多个 promise
// 异常数量:1
const p0 = Promise.reject('-');
const p1 = p0.then(() => {});
const p2 = p1.then(() => {});
三个有线性关系的 promise,却只抛出一个异常。error 数量与 promise 数量不存在直接关系
,如果没有产生新的异步操作链,多个 rejected promise 对应一个 error。
// 异常数量:2
const p0 = Promise.reject('-');
const p1 = p0.then(() => {});
const p2 = p0.then(() => {});
仅仅是把 p1、p2 对应的异步操作链中的第一步,改成 p0 对应的异步操作,异常数量立马+1。
可见除了遵循传统的异常逐层上抛机制,error(in promise)遇到异步操作链的分叉节点,会在不同的异步操作链分别上抛异常。即 error 数量与异步操作链数量存在直接关系
。
async function 情形
// 异常数量:1
const test = () => {
const rejectedP = Promise.reject('-');
rejectedP.finally(() => {});
return rejectedP;
}
// 异常数量:2。分析图如下
const asyncTest = async () => {
const rejectedP = Promise.reject('-');
rejectedP.finally(() => {});
return rejectedP; // promiseEx
} // promiseIm
对于这个现象我还只有一点猜测,如果有谬误请联系我
。
mdn 上提到 A Promise which will be resolved with the value returned by the async function,容易引起误解。经过测试,这里并不是直接调用 Promise.resolve() 方法
。如果是的话,下面的例子 P0 应该等于 p1。
const p0 = Promise.resolve(1);
const p1 = (async () => p0)();
p0 === p1; // false
而是调用了内部定义的 resolve 方法 包装显式返回值 promiseEx
。不同于 Promise.resolve ,隐式返回了新的 promiseIm,代理新的异步操作链(原因稍后解释)。且 promiseIm 的状态除了受显式返回值 promiseEx 终态的影响,还与方法内部代码有关(只要内部抛错 promiseIm 即 rejected)。
promiseIm 代理的是从调用 async function 起,一直到方法返回,同步逻辑与 await 对应的异步逻辑,再加上返回值 promiseEx 代理的异步操作链。
下面举出 promiseIm 与 promsieEx 不完全等价的例子。
// 异常数量:1。
/**
* 新生成的 promiseIm 代理的链只是 promiseEx 对应链在无分叉的扩展。虽然不等价,但是并不会制造新的分支。
*/
const extendP = async () => {
await Promise.resolve(); // resolveP
return Promise.reject('-'); // rejectP / promiseEx
} // promiseIm
我猜测,正因为两者不完全等价,所以 async function 需要生成新的包装过的异步操作链。
回过头来看,或许正是没有笼统的通过 Promise.resolve() 处理 promiseEx 充当 async function 返回值的原因。
总结
- promise 是异步操作结果的代理对象,
当 promise 代理的异步操作链失败,1 个 error(in promise) 被抛出
; - error(in promise) 抛出后,进入 promise 状态回调并逐层上抛,直到抵达顶层变为 uncaught error(in promise);
- 根据异常上抛机制,
error 数量与实际的异步操作链数量相关
。如果在上抛途中,存在新的异步操作链,生成新 error; async 方法隐式返回的 promiseIm 代理的新异步操作链,如果与显式返回值 promiseEx 代理的链分叉,异步操作链数+1,多抛出一次错误
。
补充
vue 处理实例方法返回的 promise
vue 实例定义的方法会被调用,得到的返回值追加 .catch,异常时手动进入错误收集器。因此要注意,如果在方法内生成了新的 promise,一定要返回新的 promise,否则异常抛出无法被 vue 的错误收集器监听到。