网站首页 > 博客文章 正文
大家好,很高兴又见面了,我是"高级前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。
浏览器如今已经原生支持 async/await 调用,本质上是对 Promise 的语法包装。不幸的是,async/await 的错误方式并不友好,下文将就这个主题进行深入探讨。
1.抛出错误
当异步函数中抛出错误时,可以使用 try {} catch {} 捕获。
async function fails() {
throw Error();
}
async function myFunc() {
try {
await fails();
} catch (e) {
console.log('that failed', e);
}
}
以上代码类似于以前对 Promise 错误处理的语法糖:
fails().catch((e) => {
console.log('That also failed', e);
});
事实上,在任何使用关键字 await 的地方都可以删除 await 并执行传统的.then() 和.catch() 调用, 这是因为 async 关键字隐式地为其函数创建了一个 Promise。
2.reject Promise
使用 Promises,除了使用 throw 之外,还有另一种抛出错误的方法,即调用 reject():
function fails2() {
return new Promise((resolve, reject) => {
reject(new Error());
});
}
async function myFunc2() {
try {
await fails2();
} catch (e) {
console.log('that failed', e);
}
}
传递给 reject() 的错误可以通过 try {} catch {} 和.catch() 方法捕获。
至此,有两种抛出错误以及捕获错误的方法,但是每种捕获错误的方法都能捕获两种抛出错误的方法,因此复杂性没有想象的那么高。
3.在不同的调用堆栈中抛出错误
如果自己创建 Promise,则很可能会使用 setTimeout() 或 setInterval(),或者在完成某些操作时以某种方式调用回调函数。
这些回调将在不同的调用堆栈中调用,这意味着抛出的错误将传播到不是您的代码的其他地方。考虑这个例子:
function fails3() {
return new Promise((resolve, reject) => {
setTimeout(function () {
throw new Error();
}, 100);
});
}
async function myFunc3() {
try {
await fails3();
} catch (e) {
console.log('that failed', e);
//<-- never gets called
}
产生的错误永远不会被 try {} catch {} 捕获,因为它被抛出到不同的调用堆栈上,而且使用 .catch(() => {}) 方法也会有同样的问题。
让错误在此类回调中传播的方法是使用 reject/resolve 函数,如下所示:
function fails4() {
return new Promise((resolve, reject) => {
setTimeout(function () {
reject(new Error());
}, 100);
});
}
async function myFunc4() {
try {
await fails4();
} catch (e) {
console.log('that failed', e);
//<-- this gets called
}
}
4.为什么 reject/resolve 差强人意
在 Promise 中调用 reject(new Error()) 与 throw Error() 非常相似,但有一个主要区别,即只是一个函数调用,因此它不会像 throw 那样破坏执行流程。
这意味着开发者可以编写既 reject 又 resolve 的矛盾代码,如下所示:
function schrodinger() {
return new Promise((resolve, reject) => {
reject(new Error());
// 不会破坏Promise流程
resolve('great success');
});
}
这里 reject() 和 resolve() 都会被调用。那么哪一个会赢呢?答案是先调用的哪个函数。比如下面的怪异示例:
function schr?dinger2() {
return new Promise((resolve, reject) => {
throw resolve('huh');
//<-- this throw is executed
});
}
async function callAsync() {
try {
await schr?dinger2();
} catch (e) {
console.log('caught error', e);
//<-- yet, this is never reached
}
}
try {} catch {} 永远不会被触发,这是因为调用了 resolve,而规则仍然是先调用的即获胜。 所以 throw 被执行了,但是它被运行时默默地吞掉了, 这势必会造成无尽的混乱。
发生该问题的原因是 resolve() 和 reject() 与 return 和 throw 几乎重复。 而 resolve/reject 存在的唯一原因是能够跨调用堆栈边界移动错误。
由于多种原因,这也只是一个折中的方案,因为只会移动期望的错误,因此:例如意外的 NullReferenceException 不会跨边界移动,除非自己明确地调用 reject。
有一个更简洁的设计。 在人们开始在 JavaScript 中讨论 async/await 之前,C# 就已经有了 async/await。 在该语言中,异步回调中引发的异常被捕获,然后重新引发,以便传播到正在等待异步操作的地方。
JavaScript 可以通过提供 setTimeout 和 setInterval 的替代品以及新的错误语义来实现这一点,并且可以放弃这种 resolve/reject 的东西,转而使用 return /throw, 这也会将 Promises 规范减少 90%。
5.全局处理 Promise 的错误
当开发者想要为所有未处理的错误设置全局捕获错误处理程序(例如将这些错误传递到服务器)时该怎么办?
那么,如何判断 Promise 中的错误未得到处理呢? 在处理 Promise 时,开发者无法知道将来是否会抛出错误。 Promise 可能会调用 reject(),并且一些代码可能会在 10 分钟后出现并对该 Promise 调用 .catch(() => {}),在这种情况下,错误将被处理。
因此,Q 和 Bluebird 等 Promise 库中的全局错误处理程序被命名为 onPossibleUnhandledRejection,这是一个合适的名称。
在原生 Promises 中,此函数称为 onunhandledrejection,但仍然只能判断到目前为止是否尚未处理 reject。 因此,仅当当前运行的脚本完成并且控制权已交回运行时时才会触发 onunhandledrejection(如果在此期间没有其他捕获逻辑)。
下面为 async exceptions 和 reject 设置全局处理程序,如下所示:
window.onunhandledrejection = function (evt) {
/*Your code*/
};
// 或者
window.addEventListener('unhandledrejection', function (evt) {
/*Your code*/
});
evt 是 PromiseRejectionEvent 类型的对象。 evt.promise 是被拒绝的 promise,evt.reason 保存传递给 reject() 函数的任何对象。
6.常见疑难输出
6.1 Promise + setTimeout 类型
new Promise(function (resolve, reject) {
setTimeout(() => {
throw new Error('Whoops!');
}, 1000);
}).catch(alert);
此时直接抛出错误,因为 setTimeout 内部没有通过闭包保持外部 Promise 的 resolve 和 reject 的引用,因此 Promise 的微任务执行后创建了新的宏任务,而宏任务直接在调用堆栈 Callback Stack 中抛出了错误,同时又没有全局 unhandledrejection 句柄。但是,可以通过下面方式保持对外部函数作用域的引用:
new Promise(function (resolve, reject) {
setTimeout(() => {
// 保持引用,导致外部作用域没有被回收
reject(new Error('Whoops!'));
}, 1000);
}).catch(alert);
可能会有人使用下面 try...catch 代码包裹相应逻辑,但是并不建议这么做。
new Promise(function (resolve, reject) {
setTimeout(() => {
try {
throw new Error('Whoops!');
// 注意:这里一定要有 throw,否则就变成了 new 一个Error对象
} catch (e) {
reject(e);
}
}, 1000);
}).catch(alert);
这是因为 Promise 本身会在其作用域内捕捉所有的错误,而不需要 try...catch 块。它确保在执行过程中抛出的所有异常都被获取并转换为 reject 的 Promise。
6.2 在 Promise 块内使用async/await 函数
async/await 是一种更高级的 ES6 语法,用于处理同步代码中的多个 Promise。当在一个函数声明前使用 async 关键字时,它会返回一个 Promise,此时可以使用 await 关键字来停止代码,直到正在等待的 Promise resolve 或 reject。
但是,当把一个 async 函数和 Promise 放在一起 ,可能会有一些副作用。
假设想在 Promise 块中做一个异步操作,所以使用了 async 关键字,但,不巧的是代码抛出了一个错误。这样,即使使用 catch() 块或在 try...catch 块内 await Promise,也不能立即处理这个错误。
// 此代码无法处理错误
(async () => {
try {
await new Promise(async () => {
// async函数放在Promise里面
throw new Error('message');
});
} catch (e) {
console.log(e.message);
}
})();
只能按照如下方式自行捕获错误:
new Promise(async (resolve, reject) => {
try {
throw new Error('message');
} catch (error) {
reject(error);
}
}).catch((e) => console.log(e.message));
//using async/await
(async () => {
try {
await new Promise(async (resolve, reject) => {
try {
throw new Error('message');
} catch (error) {
reject(error);
}
});
} catch (e) {
console.log(e.message);
}
})();
6.3 then 方法继续返回一个 Promise
Promise 的 then 方法里面可以返回以下三种类型。
- return 一个 promise 对象
- return 一个同步的值或者是 undefined
返回 undefined 通常是一个错误,但返回同步值实际上是将同步代码转换为 Promise 代码的绝佳方法。例如,假设有一个用户的内存缓存,可以这么做:
getUserByName('nolan')
.then(function (user) {
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id];
// returning a synchronous value!
}
return getUserAccountById(user.id);
// returning a promise!
})
.then(function (userAccount) {
// I got a user account!
});
第二个函数不关心 userAccount 是同步还是异步获取,第一个函数可以自由返回同步或异步值。
不幸的是,有一个事实:JavaScript 中的非返回函数从技术上讲返回 undefined,这意味着当打算返回某些内容时很容易意外引入副作用。
建议大家养成总是从 then() 函数内部返回或抛出 Error 个人习惯。
- 同步的 throw 一个错误
假设想在用户注销时抛出同步错误,可以如下设置:
getUserByName('nolan')
.then(function (user) {
if (user.isLoggedOut()) {
throw new Error('user logged out!');
// 抛出同步错误
}
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id];
// 返回同步值
}
return getUserAccountById(user.id);
// 返回一个Promise
})
.then(function (userAccount) {
// I got a user account!
})
.catch(function (err) {
// Boo, I got an error!
});
如果用户注销, catch() 将收到同步错误,如果任何 Promise 被 Reject,将收到异步错误。 同样,该函数并不关心收到的错误是同步还是异步。
这特别有用,因为可以帮助识别开发过程中的编码错误。 例如:如果在 then() 函数内部的任何点执行 JSON.parse(),如果 JSON 无效,它可能会抛出同步错误。 使用回调,该错误将被吞噬,但使用 Promise,可以简单地在 catch() 函数中处理。
6.4 then 方法传入 Promise 对象而非函数
比如下面的示例代码:
Promise.resolve('foo')
.then(Promise.resolve('bar'))
.then(function (result) {
console.log(result);
});
如果认为会打印出 bar,那就错了,实际上会打印 foo!
发生这种情况的原因是,当向 then() 传递一个非函数(例如:Promise)时,实际上将其解释为 then(null),这会导致前一个 Promise 的结果落空,比如下面的代码:
Promise.resolve('foo')
.then(null)
// 注意:这里的null可以换成任何内容,只要不是函数
.then(function (result) {
console.log(result);
});
添加任意数量的 then(null) 仍然会打印 foo。简而言之,可以将 Promise 直接传递到 then() 方法中,但它不会执行认为它正在执行的操作。 then() 应该接受一个函数,所以可以如下处理:
Promise.resolve('foo')
.then(function () {
return Promise.resolve('bar');
})
.then(function (result) {
console.log(result);
});
正如期望的,程序输出'bar'。 所以提醒一下,始终将函数传递给 then()!
6.5 then 支持两个参数
catch()只是糖,所以这两个片段是等价的:
somePromise().catch(function (err) {
// handle error
});
somePromise().then(null, function (err) {
// handle error
});
但是,这并不意味以下两个片段是等效的:
somePromise()
.then(function () {
throw new Error('oh noes');
})
.catch(function (err) {
// I caught your error! :)
});
somePromise().then(
function () {
throw new Error('oh noes');
},
function (err) {
// I didn't catch your error! :(
}
);
事实证明,当使用 then(resolveHandler,rejectHandler) 格式时,rejectHandler 实际上不会捕获由 resolveHandler 本身抛出的错误。因此,非常建议不使用 then() 的第二个参数,而总是使用 catch()。
6.6 Promise.resolve() 防止错误被吞噬
Promise 对于将同步代码包装为异步代码非常有用。但是,如果发现自己经常输入以下内容:
new Promise(function (resolve, reject) {
resolve(someSynchronousValue);
}).then(/* ... */);
则可以使用 Promise.resolve() 更简洁地方式:
Promise.resolve(someSynchronousValue).then(/* ... */);
这种方法对于捕获任何同步错误也非常有用:
function doSomethingThatMayThrow() {
//throw new Error('同步错误抛出了');
}
function somePromiseAPI() {
return Promise.resolve()
.then(function () {
doSomethingThatMayThrow();
return 'foo';
})
.then((res) => alert(res))
.catch((e) => alert('catch:' + e));
// 能catch住
}
somePromiseAPI().then(res=>alert('res了'));
请记住:任何可能同步抛出错误的代码都会增加错误被吞噬的可能性,但如果将所有内容都包装在 Promise.resolve() 中,那么错误总是可以确保被 catch 住。
类似地,可以使用 Promise.reject() 返回立即拒绝的 Promise:
Promise.reject(new Error('some awful error'));
参考资料
https://catchjs.com/Docs/AsyncAwait
https://www.studocu.com/row/document/universite-sidi-mohamed-ben-abdellah-de-fes/informatique/stop-using-try-catch-to-catch-async-await-exceptions-by-omgzui-may-2023-java-script-in-plain-english/64051536
https://github.com/liangklfangl/react-article-bucket/blob/master/async-programing/readme.md
https://blog.csdn.net/iteye_3854/article/details/82677078
https://javascript.info/promise-error-handling
https://github.com/liangklfangl/react-article-bucket/blob/master/others/nodejs-QA/browser-QA.md
https://segmentfault.com/a/1190000042773867
https://blog.csdn.net/iteye_3854/article/details/82677078
https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
https://dmitripavlutin.com/javascript-promises-then-vs-then-catch/
https://tomduffytech.com/unhandled-promise-rejection/
猜你喜欢
- 2024-11-09 ES6 fetch()方法详解(es6find方法)
- 2024-11-09 ES6新增语法(七)——async...await
- 2024-11-09 手把手一行一行代码教你手写Promise
- 2024-11-09 把 Node.js 中的回调转换为 Promise
- 2024-11-09 BAT前端经典面试问题 es6之promise原理,promise应用场景
- 2024-11-09 ES6 完全使用手册附加案例实战讲解
- 2024-11-09 ES6 的常用新特性(es6特性及使用场景)
- 2024-11-09 Es6基础语法(es6语法重点整理)
- 2024-11-09 「新手向」 Promise课程笔记整理(promise yo)
- 2024-11-09 ES6中的Promise.resolve()到底有何作用呢
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- powershellfor (55)
- messagesource (56)
- aspose.pdf破解版 (56)
- promise.race (63)
- 2019cad序列号和密钥激活码 (62)
- window.performance (66)
- qt删除文件夹 (72)
- mysqlcaching_sha2_password (64)
- ubuntu升级gcc (58)
- nacos启动失败 (64)
- ssh-add (70)
- jwt漏洞 (58)
- macos14下载 (58)
- yarnnode (62)
- abstractqueuedsynchronizer (64)
- source~/.bashrc没有那个文件或目录 (65)
- springboot整合activiti工作流 (70)
- jmeter插件下载 (61)
- 抓包分析 (60)
- idea创建mavenweb项目 (65)
- vue回到顶部 (57)
- qcombobox样式表 (68)
- vue数组concat (56)
- tomcatundertow (58)
- pastemac (61)
本文暂时没有评论,来添加一个吧(●'◡'●)