JS异步错误捕获的一些事小结

引入

我们都知道 try catch 无法捕获 setTimeout 异步任务中的错误,那其中的原因是什么。以及异步代码在 js 中是特别常见的,我们该怎么做才比较?

无法捕获的情况

function main() {
 try {
 setTimeout(() => {
 throw new Error('async error')
 }, 1000)
 } catch(e) {
 console.log(e, 'err')
 console.log('continue...')
 }
}

main();

这段代码中,setTimeout 的回调函数抛出一个错误,并不会在 catch 中捕获,会导致程序直接报错崩掉。

所以说在 js 中 try catch 并不是说写上一个就可以高枕无忧了。难道每个函数都要写吗,
那什么情况下 try catch 无法捕获 error 呢?

异步任务

  • 宏任务的回调函数中的错误无法捕获

上面的栗子稍微改一下,主任务中写一段 try catch,然后调用异步任务 task,task 会在一秒之后抛出一个错误。

// 异步任务
const task = () => {
 setTimeout(() => {
 throw new Error('async error')
 }, 1000)
}
// 主任务
function main() {
 try {
 task();
 } catch(e) {
 console.log(e, 'err')
 console.log('continue...')
 }
}

这种情况下 main 是无法 catch error 的,这跟浏览器的执行机制有关。异步任务由 eventloop 加入任务队列,并取出入栈(js 主进程)执行,而当 task 取出执行的时候, main 的栈已经退出了,也就是上下文环境已经改变,所以 main 无法捕获 task 的错误。

事件回调,请求回调同属 tasks,所以道理是一样的。eventloop 复习可以看这篇文章

  • 微任务(promise)的回调
// 返回一个 promise 对象
const promiseFetch = () =>
 new Promise((reslove) => {
 reslove();
})

function main() {
 try {
 // 回调函数里抛出错误
 promiseFetch().then(() => {
 throw new Error('err')
 })
 } catch(e) {
 console.log(e, 'eeee');
 console.log('continue');
 }
}

promise 的任务,也就是 then 里面的回调函数,抛出错误同样也无法 catch。因为微任务队列是在两个 task 之间清空的,所以 then 入栈的时候,main 函数也已经出栈了。

并不是回调函数无法 try catch

很多人可能有一个误解,因为大部分遇到无法 catch 的情况,都发生在回调函数,就认为回调函数不能 catch。

不全对,看一个最普通的栗子。

// 定义一个 fn,参数是函数。
const fn = (cb: () => void) => {
 cb();
};

function main() {
 try {
 // 传入 callback,fn 执行会调用,并抛出错误。
 fn(() => {
 throw new Error('123');
 })
 } catch(e) {
 console.log('error');
 }
}
main();

结果当然是可以 catch 的。因为 callback 执行的时候,跟 main 还在同一次事件循环中,即一个 eventloop tick。所以上下文没有变化,错误是可以 catch 的。

根本原因还是同步代码,并没有遇到异步任务。

promise 的异常捕获

构造函数

先看两段代码:

function main1() {
 try {
 new Promise(() => {
 throw new Error('promise1 error')
 })
 } catch(e) {
 console.log(e.message);
 }
}

function main2() {
 try {
 Promise.reject('promise2 error');
 } catch(e) {
 console.log(e.message);
 }
}

以上两个 try catch 都不能捕获到 error,因为 promise 内部的错误不会冒泡出来,而是被 promise 吃掉了,只有通过 promise.catch 才可以捕获,所以用 Promise 一定要写 catch 啊。

然后我们再来看一下使用 promise.catch 的两段代码:

// reject
const p1 = new Promise((reslove, reject) => {
 if(1) {
 reject();
 }
});
p1.catch((e) => console.log('p1 error'));
// throw new Error
const p2 = new Promise((reslove, reject) => {
 if(1) {
 throw new Error('p2 error')
 }
});

p2.catch((e) => console.log('p2 error'));

promise 内部的无论是 reject 或者 throw new Error,都可以通过 catch 回调捕获。

这里要跟我们最开始微任务的栗子区分,promise 的微任务指的是 then 的回调,而此处是 Promise 构造函数传入的第一个参数,new Promise 是同步执行的。

then

那 then 之后的错误如何捕获呢。

function main3() {
 Promise.resolve(true).then(() => {
 try {
 throw new Error('then');
 } catch(e) {
 return e;
 }
 }).then(e => console.log(e.message));
}

只能是在回调函数内部 catch 错误,并把错误信息返回,error 会传递到下一个 then 的回调。

用 Promise 捕获异步错误

const p3 = () => new Promise((reslove, reject) => {
 setTimeout(() => {
 reject('async error');
 })
});

function main3() {
 p3().catch(e => console.log(e));
}
main3();

把异步操作用 Promise 包装,通过内部判断,把错误 reject,在外面通过 promise.catch 捕获。

async/await 的异常捕获

首先我们模拟一个请求失败的函数 fetchFailure,fetch 函数通常都是返回一个 promise。

main 函数改成 async,catch 去捕获 fetchFailure reject 抛出的错误。能不能获取到呢。

const fetchFailure = () => new Promise((resolve, reject) => {
 setTimeout(() => {// 模拟请求
 if(1) reject('fetch failure...');
 })
})

async function main () {
 try {
 const res = await fetchFailure();
 console.log(res, 'res');
 } catch(e) {
 console.log(e, 'e.message');
 }
}
main();

async 函数会被编译成好几段,根据 await 关键字,以及 catch 等,比如 main 函数就是拆成三段。

1.fetchFailure 2. console.log(res) 3. catch

通过 step 来控制迭代的进度,比如 "next",就是往下走一次,从 1->2,异步是通过 Promise.then() 控制的,你可以理解为就是一个 Promise 链,感兴趣的可以去研究一下。 关键是生成器也有一个 "throw" 的状态,当 Promise 的状态 reject 后,会向上冒泡,直到 step('throw') 执行,然后 catch 里的代码 console.log(e, 'e.message'); 执行。

明显感觉 async/await 的错误处理更优雅一些,当然也是内部配合使用了 Promise。

更进一步

async 函数处理异步流程是利器,但是它也不会自动去 catch 错误,需要我们自己写 try catch,如果每个函数都写一个,也挺麻烦的,比较业务中异步函数会很多。

首先想到的是把 try catch,以及 catch 后的逻辑抽取出来。

const handle = async (fn: any) => {
 try {
 return await fn();
 } catch(e) {
 // do sth
 console.log(e, 'e.messagee');
 }
}

async function main () {
 const res = await handle(fetchFailure);
 console.log(res, 'res');
}

写一个高阶函数包裹 fetchFailure,高阶函数复用逻辑,比如此处的 try catch,然后执行传入的参数-函数 即可。

然后,加上回调函数的参数传递,以及返回值遵守 first-error,向 node/go 的语法看齐。如下:

const handleTryCatch = (fn: (...args: any[]) => Promise<{}>) => async (...args: any[]) => {
 try {
 return [null, await fn(...args)];
 } catch(e) {
 console.log(e, 'e.messagee');
 return [e];
 }
}

async function main () {
 const [err, res] = await handleTryCatch(fetchFailure)('');
 if(err) {
 console.log(err, 'err');
 return;
 }
 console.log(res, 'res');
}

但是还有几个问题,一个是 catch 后的逻辑,这块还不支持自定义,再就是返回值总要判断一下,是否有 error,也可以抽象一下。

所以我们可以在高阶函数的 catch 处做一下文章,比如加入一些错误处理的回调函数支持不同的逻辑,然后一个项目中错误处理可以简单分几类,做不同的处理,就可以尽可能的复用代码了。

// 1. 三阶函数。第一次传入错误处理的 handle,第二次是传入要修饰的 async 函数,最后返回一个新的 function。

const handleTryCatch = (handle: (e: Error) => void = errorHandle) =>
 (fn: (...args: any[]) => Promise<{}>) => async(...args: any[]) => {
 try {
 return [null, await fn(...args)];
 } catch(e) {
 return [handle(e)];
 }
 }

// 2. 定义各种各样的错误类型
// 我们可以把错误信息格式化,成为代码里可以处理的样式,比如包含错误码和错误信息
class DbError extends Error {
 public errmsg: string;
 public errno: number;
 constructor(msg: string, code: number) {
 super(msg);
 this.errmsg = msg || 'db_error_msg';
 this.errno = code || 20010;
 }
}
class ValidatedError extends Error {
 public errmsg: string;
 public errno: number;
 constructor(msg: string, code: number) {
 super(msg);
 this.errmsg = msg || 'validated_error_msg';
 this.errno = code || 20010;
 }
}

// 3. 错误处理的逻辑,这可能只是其中一类。通常错误处理都是按功能需求来划分
// 比如请求失败(200 但是返回值有错误信息),比如 node 中写 db 失败等。
const errorHandle = (e: Error) => {
 // do something
 if(e instanceof ValidatedError || e instanceof DbError) {
 // do sth
 return e;
 }
 return {
 code: 101,
 errmsg: 'unKnown'
 };
}
const usualHandleTryCatch = handleTryCatch(errorHandle);

// 以上的代码都是多个模块复用的,那实际的业务代码可能只需要这样。
async function main () {
 const [error, res] = await usualHandleTryCatch(fetchFail)(false);
 if(error) {
 // 因为 catch 已经做了拦截,甚至可以加入一些通用逻辑,这里甚至不用判断 if error
 console.log(error, 'error');
 return;
 }
 console.log(res, 'res');
}

解决了一些错误逻辑的复用问题之后,即封装成不同的错误处理器即可。但是这些处理器在使用的时候,因为都是高阶函数,可以使用 es6 的装饰器写法。

不过装饰器只能用于类和类的方法,所以如果是函数的形式,就不能使用了。不过在日常开发中,比如 React 的组件,或者 Mobx 的 store,都是以 class 的形式存在的,所以使用场景挺多的。

比如改成类装饰器:

const asyncErrorWrapper = (errorHandler: (e: Error) => void = errorHandle) => (target: Function) => {
 const props = Object.getOwnPropertyNames(target.prototype);
 props.forEach((prop) => {
 var value = target.prototype[prop];
 if(Object.prototype.toString.call(value) === '[object AsyncFunction]'){
 target.prototype[prop] = async (...args: any[]) => {
 try{
 return await value.apply(this,args);
 }catch(err){
 return errorHandler(err);
 }
 }
 }
 });
}

@asyncErrorWrapper(errorHandle)
class Store {
 async getList (){
 return Promise.reject('类装饰:失败了');
 }
}

const store = new Store();

async function main() {
 const o = await store.getList();
}
main();

这种 class 装饰器的写法是看到黄子毅这么写过,感谢灵感。

koa 的错误处理

如果对 koa 不熟悉,可以选择跳过不看。

koa 中当然也可以用上面 async 的做法,不过通常我们用 koa 写 server 的时候,都是处理请求,一次 http 事务会掉起响应的中间件,所以 koa 的错误处理很好的利用了中间件的特性。

比如我的做法是,第一个中间件为捕获 error,因为洋葱模型的缘故,第一个中间件最后仍会执行,而当某个中间件抛出错误后,我期待能在此捕获并处理。

// 第一个中间件
const errorCatch = async(ctx, next) => {
 try {
 await next();
 } catch(e) {
 // 在此捕获 error 路由,throw 出的 Error
 console.log(e, e.message, 'error');
 ctx.body = 'error';
 }
}

app.use(errorCatch);

// logger
app.use(async (ctx, next) => {
 console.log(ctx.req.body, 'body');
 await next();
})

// router 的某个中间件
router.get('/error', async (ctx, next) => {
 if(1) {
 throw new Error('错误测试')
 }
 await next();
})

为什么在第一个中间件写上 try catch,就可以捕获前面中间件 throw 出的错误呢。首先我们前面 async/await 的地方解释过,async 中await handle(),handle 函数内部的 throw new Error 或者 Promise.reject() 是可以被 async 的 catch 捕获的。所以只需要 next 函数能够拿到错误,并抛出就可以了,那看看 next 函数。

// compose 是传入中间件的数组,最终形成中间件链的,next 控制游标。
 compose(middlewares) {
 return (context) => {
 let index = 0;
 // 为了每个中间件都可以是异步调用,即 `await next()` 这种写法,每个 next 都要返回一个 promise 对象

 function next(index) {
 const func = middlewares[index];
 try {
 // 在此处写 try catch,因为是写到 Promise 构造体中的,所以抛出的错误能被 catch
 return new Promise((resolve, reject) => {
 if (index >= middlewares.length) return reject('next is inexistence');
 resolve(func(context, () => next(index + 1)));
 });
 } catch(err) {
 // 捕获到错误,返回错误
 return Promise.reject(err);
 }
 }
 return next(index);
 }
 }

next 函数根据 index,取出当前的中间件执行。中间件函数如果是 async 函数,同样的转化为 generator 执行,内部的异步代码顺序由它自己控制,而我们知道 async 函数的错误是可以通过 try catch 捕获的,所以在 next 函数中加上 try catch 捕获中间件函数的错误,再 return 抛出去即可。所以我们才可以在第一个中间件捕获。详细代码可以看下简版 koa

然后 koa 还提供了 ctx.throw 和全局的 app.on 来捕获错误。

如果你没有写错误处理的中间件,那可以使用 ctx.throw 返回前端,不至于让代码错误。

但是 throw new Error 也是有优势的,因为某个中间件的代码逻辑中,一旦出现我们不想让后面的中间件执行,直接给前端返回,直接抛出错误即可,让通用的中间件处理,反正都是错误信息。

// 定义不同的错误类型,在此可以捕获,并处理。
const errorCatch = async(ctx, next) => {
 try {
 await next();
 } catch (err) {
 const { errmsg, errno, status = 500, redirect } = err;

 if (err instanceof ValidatedError || err instanceof DbError || err instanceof AuthError || err instanceof RequestError) {
 ctx.status = 200;
 ctx.body = {
 errmsg,
 errno,
 };
 return;
 }
 ctx.status = status;
 if (status === 302 && redirect) {
 console.log(redirect);
 ctx.redirect(redirect);
 }
 if (status === 500) {
 ctx.body = {
 errmsg: err.message,
 errno: 90001,
 };
 ctx.app.emit('error', err, ctx);
 }
 }
}

app.use(errorCatch);

// logger
app.use(async (ctx, next) => {
 console.log(ctx.req.body, 'body');
 await next();
})

// 通过 ctx.throw
app.use(async (ctx, next) => {
 //will NOT log the error and will return `Error Message` as the response body with status 400
 ctx.throw(400,'Error Message');
}); 

// router 的某个中间件
router.get('/error', async (ctx, next) => {
 if(1) {
 throw new Error('错误测试')
 }
 await next();
})

// 最后的兜底
app.on('error', (err, ctx) => {
 /* centralized error handling:
 * console.log error
 * write error to log file
 * save error and request information to database if ctx.request match condition
 * ...
 */
});

最后

本文的代码都存放于

总的来说,目前 async 结合 promise 去处理 js 的异步错误会是比较方便的。另外,成熟的框架(react、koa)对于错误处理都有不错的方式,尽可能去看一下官方是如何处理的。

这只是我对 js 中处理异步错误的一些理解。不过前端的需要捕获异常的地方有很多,比如前端的代码错误,cors 跨域错误,iframe 的错误,甚至 react 和 vue 的错误我们都需要处理,以及异常的监控和上报,以帮助我们及时的解决问题以及分析稳定性。采取多种方案应用到我们的项目中,让我们不担心页面挂了,或者又报 bug 了,才能安安稳稳的去度假休息😆

最后的最后,blog地址: https://github.com/sunyongjia...

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • Javascript 异步加载详解(浏览器在javascript的加载方式)

    一.同步加载与异步加载的形式 1. 同步加载 我们平时最常使用的就是这种同步加载形式: <script src="http://yourdomain.com/script.js"></script> 同步模式,又称阻塞模式,会阻止浏览器的后续处理,停止了后续的解析,因此停止了后续的文件加载(如图像).渲染.代码执行. js 之所以要同步执行,是因为 js 中可能有输出 document 内容.修改dom.重定向等行为,所以默认同步执行才是安全的. 以前的一般建议

  • javascript 文件的同步加载与异步加载实现原理

    HTML 4.01 的script属性 charset: 可选.指定src引入代码的字符集,大多数浏览器忽略该值. defer: boolean, 可选.延迟脚本执行,相当于将script标签放入页面body标签的底部,js脚本会在document的DOMContentLoaded之前执行.除IE和较新版本的Firefox外,其他浏览器并未支持. language: 已废弃.大部分浏览器会忽略该值. src: 可选.指定引入的外部代码文件,不限制后缀名. type: 必选.指定脚本的内容类型(M

  • js中同步与异步处理的方法和区别总结

    在使用异步请求时,有时需要将异步请求的结果返回给另一个js函数,此种情况下会出现未等异步请求返回请求结果,该发送请求所在js函数已经执行完后续操作,即已经执行return ,这样会导致return的结果为空字符. 总结:若要在使用ajax请求后处理发送请求返回的结果,最好使用同步请求. 例如:以下例子会出现返回结果不正确的情况,因为ajax异步请求还未执行完,函数已经执行return了, 复制代码 代码如下: function fn(){ var result = " "; $.aja

  • NodeJS处理Express中异步错误

    摘要 比起回调函数,使用 Promise 来处理异步错误要显得优雅许多. 结合 Express 内置的错误处理机制和 Promise 极大地降低产生未捕获错误(uncaught exception)的可能性. Promise 在ES6中是默认选项.如果使用 Babel 转译,它也可以与 Generators 或者 Async/Await 相结合. 本文主要阐述如何在 Express 中使用错误处理中间件(error-handling middleware)来高效处理异步错误.在 Github 上

  • js异步加载的三种解决方案

    默认情况javascript是同步加载的,也就是javascript的加载时阻塞的,后面的元素要等待javascript加载完毕后才能进行再加载,对于一些意义不是很大的javascript,如果放在页头会导致加载很慢的话,是会严重影响用户体验的. (1) defer,只支持IE defer属性的定义和用法(我摘自w3school网站) defer 属性规定是否对脚本执行进行延迟,直到页面加载为止. 有的 javascript 脚本 document.write 方法来创建当前的文档内容,其他脚本

  • 用window.onerror捕获并上报Js错误的方法

    前两天有个2048游戏的用户反馈说,打开游戏后不能玩儿,只有一个游戏面板,数字无法初始化,更无法移动,设备为iPhone 4S.iOS 5.1.尝试从微信调起Safari打开,依然不好使.由于游戏中运用了比较多的HTML5特性,所以粗略估计是有JS报错导致.不过这样的信息该如何捕获到呢?当然是传说中的window.onerror. 从W3C找到关于window.onerror的方法体介绍: 这个意思,基本可以就是说,window.onerror方法,我们可以写成: /** * @param {S

  • JS异步错误捕获的一些事小结

    引入 我们都知道 try catch 无法捕获 setTimeout 异步任务中的错误,那其中的原因是什么.以及异步代码在 js 中是特别常见的,我们该怎么做才比较? 无法捕获的情况 function main() { try { setTimeout(() => { throw new Error('async error') }, 1000) } catch(e) { console.log(e, 'err') console.log('continue...') } } main(); 这

  • 浅谈如何优雅处理JavaScript异步错误

    1. try/catch try/catch基本上是大家最常和async/await一起使用的,基本上我们会用它去包围大部分的异步方法.await关键字后面的promise一旦reject了,就会抛出一个异常错误. run(); async function run() { try { await Promise.ject(new Error('Oops!')); } catch (err) { console.error(error.message); } } try/catch同样也可以处理

  • JavsScript中Promise的错误捕获详解

    目录 我们需要在异步任务中准确的进行错误捕获,以便我们可以知道错误出在什么地方 我们再讨论then方法中的第二个参数和Promise.catch方法的区别 题: then方法的连续调用,怎么能够知道是第几个then方法报错了呢. 总结 我们需要在异步任务中准确的进行错误捕获,以便我们可以知道错误出在什么地方 如果对Promise和trycatch不够理解的话,很多时候会出现Promise中的错误无法被捕获的情况,本文来讨论这些情况 try catch try catch 只能捕获当前上下文中的错

  • JS异步堆栈追踪之为什么await胜过Promise

    概述 async/await和Promise的根本区别在于await fn()暂停当前函数的执行,而promise.then(fn)在将fn调用添加到回调链后,继续执行当前函数. const fn = () => console.log('hello') const a = async () => { await fn() // 暂停 fn 的执行 } // 调用 a 时,才恢复 fn 的执行 a() // "hello" const promise = Promise.r

  • 7个你应该知道的JS原生错误类型

    概述 从浏览器控制台到运行 Node.js的终端,我们到处都会看到错误.本文的重点是概述我们在js开发过程中可能遇到的错误类型. 提示:良好的错误提示会导致快速而无痛的发展经历与缓慢而痛苦的发展经历之间的区别.在编写可重用的代码时,请确保自己在编写清晰易懂的错误处理代码. 1. RangeError 当数字超出允许的值范围时,将会抛出此错误. 例如 const l = console.logconst arr = [90,88] arr.length=90**99 我们有一个数组,带有两个元素的

  • 防止Node.js中错误导致进程阻塞的办法

    前言 在Node.js中,当某个回调函数发生了错误,整个进程都会崩溃,影响后面的代码执行. Node.js这样处理,是因为在发生未被捕获的错误时,进程的状态就不确定.之后也就无法正常工作了.如果错误始终不处理的话,就回一直抛出意料之外的错误,这样不利于调试. 防止错误导致的进程阻塞的方法主要有如下两种: 一. try-catch try-catch允许进行异常捕获,并让代码继续执行下去: 例如: 当函数抛出错误时,代码就停止执行了: (function() { var a = 0; a(); c

  • JS异步函数队列功能实例分析

    本文实例讲述了JS异步函数队列功能.分享给大家供大家参考,具体如下: 场景: 做直播,会有入场消息,入场特效,用户如果有坐骑,需要给他展示几秒钟的坐骑特效,如果几个人同时进场,那该怎么展示呢?这时候就会想到setTimeout函数,对,思路不错,但是,异步函数队列怎么实现呢?直接上代码: var Queue = function() { this.list = []; }; Queue.prototype = { constructor: Queue, queue: function(fn) {

  • JS异步执行结果获取的3种解决方式

    前言 JS异步执行机制具有非常重要的地位,尤其体现在回调函数和事件等方面. 但异步有时候很方便,有时候却很让人恼火,下面来总结一下异步执行结果获取的方法 回调 这是最传统的方法了,也是最简单的,如下代码 function foo(cb) { setTimeout(function() { cb(1); // 通过参数把结果返回 }, 2000); } foo(function(result) { // 调用foo方法的时候,通过回调把方法返回的数据取出来 console.log(result);

  • js事件机制----捕获与冒泡机制实例分析

    本文实例讲述了js事件机制----捕获与冒泡机制.分享给大家供大家参考,具体如下: 先从事件绑定机制说起, 事件绑定机制通过绑定方法addEventListener()实现, 语法格式如下: element.addEventListener(event, function, useCapture) 参数值 参数 描述 event 必须.字符串,指定事件名. 注意: 不要使用 "on" 前缀. 例如,使用 "click" ,而不是使用 "onclick&qu

随机推荐