如何优雅地在Node应用中进行错误异常处理


Posted in Javascript onNovember 25, 2019

不知道你有没有遇到这样一种情况,某天你写的代码在线上突然发生错误,然后你打开控制台,却对着打过包的错误信息毫无头绪?又或者说是代码在node端出现了问题,你查看错误日志的时候,却发现日志文件中都是杂乱的错误堆栈信息。

其实上面这些问题都可以通过在代码中引入合适的错误机制进行解决。大部分时候,由于程序员在开发过程中更加关注需求的实现,反而会忽视一些底层的工作。而错误处理机制就相当于我们代码上的最后一道保险,在程序发生已知或者意外的问题的时候,可以让开发者在第一时间获取信息,从而快速定位并解决问题。

常用的错误处理机制

首先我们来了解一下目前前端领域到底有哪些错误处理机制。

try catch

try...catch这种错误处理机制一定是大家最熟悉的,Javascript语言内置的错误处理机制可以在检测到代码异常的时候直接进行捕获并处理。

function test() {
 try {
 throw new Error("error");
 } catch(err) {
 console.log("some error happened:");
 }
}

 
test()

node原生错误处理机制

大多数Node.js核心API都提供的是利用回调函数处理错误,例如:

const fs = require('fs');

function read() {
 fs.readFile("/some/file/does-not-exist", (err, data) => {
 if(err) {
  throw new Error("file not exist");
 }
 console.log(data);
 });
}

read();

通过回调函数的err参数来检查是否出现错误,再进行处理。之所以Node.js采用这种错误处理机制,是因为异步方法所产生的方法并不能简单地通过try...catch机制进行拦截。

promise

Promise是用于处理异步调用的规范,而其提供的错误处理机制,是通过catch方法进行捕获。

fs.mkdir("./temp").then(() => {
 fs.writeFile("./temp/foobar.txt", "hello");
}).catch(err => {
 console.log(err)
});

async/await + try catch

第三种错误处理机制是采用async/await语法糖加上try...catch语句进行的。这样做的好处是异步和同步调用都能够使用统一的方式进行处理了。

async function one() {
 await two();
}

async function two() {
 await "hello";
 throw new Error("error");
}

async function test() {
 try {
 await one();
 } catch(error) {
 console.log(error);
 }
}

test();

解决方案

promisify

如果你的代码中充斥着多种不同的错误处理模式,那么维护起来是十分困难的。而且代码的可读性也会大大降低。因此,这里推荐采用的统一的解决方案。对于同步代码来说,直接使用try...catch方式进行捕获处理即可,而对于异步代码来说,建议转换成Promise然后采用async/await + try...catch这种方式进行处理。这样风格统一,程序的健壮性也大大加强。例如下面这个数据库请求的代码:

const database = require("database");

function promiseGet(query) {
 return new Promise((resolve, reject) => {
  database.get(query, (err, result) => {
   if (err) {
    reject(err);
   } else {
    resolve(result);
   }
  })
 })
}

async function main() {
 await promiseGet("foo");
}

main();

自定义错误类型

直接使用系统原生的错误信息通常会显得太过单薄,不利于后续进一步的分析和处理。所以为了让代码的错误处理机制的功能更加强大,我们势必要多花点精力进行额外的改造。

可以通过扩展基础的Error类型来达到这一目的。

一般来说,要根据错误发生的位置采用不同的错误类型。

首先是应用层错误,它会保存额外的线索数据:

class ApplicationError extends Error {
 constructor(message, options = {}) {
 assert(typeof message === 'string');
 assert(typeof options === 'object');
 assert(options !== null);
 super(message);

 // Attach relevant information to the error instance
 // (e.g., the username).
 for (const [key, value] of Object.entries(options)) {
  this[key] = value;
 }
 }

 get name() {
 return this.constructor.name;
 }
}

接着,可以再定义用户接口的错误类型,该类型主要用于直接返回给客户端,比如错误状态码等等。

class UserFacingError extends ApplicationError {
 constructor(message, options = {}) {
 super(message, options);
 }
}

class BadRequestError extends UserFacingError {
 get statusCode() {
 return 400
 }
}

class NotFoundError extends UserFacingError {
 get statusCode() {
 return 404
 }
}

另外,对于底层的数据库错误来说,可能需要更加细节的错误信息。此时也可以根据业务需要进行自定义:

class DatabaseError extends ApplicationError {
 get toString() {
 return "Errored happend in query: " + this.query + "\n" + this.message;
 }
}

// 使用的话
throw new DatabaseError("Other message", {
 query: query
});

化繁为简,集中处理

express

有了基础的错误数据类型后,我们可以在代码里针对不同的错误类型采取不同的解决方案。 接下来,以Express应用为例讲解一下使用方法。

app.use('/user', (req, res, next) => {
 const data = await database.getData(req.params.userId);
 if (!data) {
 throw new NotFoundError("User not found")
 }
 
 // do other thing
});

// 错误处理中间件
app.use(async (err, req, res, next) => {
 if (err instanceof UserFacingError) {
 res.sendStatus(err.statusCode);
 // or
 res.status(err.statusCode).send(err.errorCode)
 } else {
 res.sendStatus(500)
 }
 
 // 记录日志
 await logger.logError(err, 'parameter:', req.params, 'User Data:', req.user);
 // 发送邮件
 await sendMailToAdminIfCritical();
})

具体到实际场景中,需要在不同的路由中抛出不同的错误类型,然后我们就可以通过在错误处理中间件中进行统一的处理。比如根据不同的错误类型返回不同的错误码。还可以进行记录日志,发送邮件等操作。

database

数据库发生错误的时候,除了常规的抛出错误,有时候你可能还需要进行额外的重试或回退操作,如:

// 发生网络错误的时候隔200ms,重试3次
function query(queryStr, token, repeatTime = 0, delay = 200) {
 try {
 await db.query(queryStr);
 } catch (err) {
 if (err instanceof NetworkError && repeatTime < 3) {
  query(queryStr, token, repeatTime + 1, delay);
 }
 
 throw err;
 }
}

未处理错误

对于未处理的错误来说,我们可以使用node.js的unhandledRejection事件进行监听:

process.on('unhandledRejection', error => {

 console.error('unhandledRejection', error);
 // To exit with a 'failure' code
 process.exit(1);
});

而且从Node.js 12.0开始,可以使用以下命令启动程序:

node app.js --unhandled-rejections

这样也能够在发现未处理异常的时候进行处理,官方支持了三种对应的处理模式:

strict: Raise the unhandled rejection as an uncaught exception.

warn: Always trigger a warning, no matter if the unhandledRejection hook is set or not but do not print the deprecation warning.

none: Silence all warnings.

总结

最后,总结一下。为了实现可扩展和可维护的错误处理机制,我们可以需要注意以下几个方面:

  • 使用自定义Error类,后续还能根据业务需要进行扩展
  • 将异步代码转换成Promise,然后统一使用async/await + try...catch的形式进行错误捕获
  • 尽量采用统一的错误处理中间件函数
  • 保持Error信息可理解,返回合适的错误状态和代码
  • 对于未处理的错误,要即使捕获并记录

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
一组JS创建和操作表格的函数集合
May 07 Javascript
jquery 元素相对定位代码
Oct 15 Javascript
jQuery实现鼠标划过修改样式的方法
Apr 14 Javascript
功能强大的Bootstrap组件(结合js)
Aug 03 Javascript
node.js平台下的mysql数据库配置及连接
Mar 31 Javascript
微信小程序首页的分类功能和搜索功能的实现思路及代码详解
Sep 11 Javascript
JS中通过url动态获取图片大小的方法小结(两种方法)
Oct 31 Javascript
Vue组件内部实现一个双向数据绑定的实例代码
Apr 04 Javascript
vue element upload组件 file-list的动态绑定实现
Oct 11 Javascript
javascript设计模式 ? 状态模式原理与用法实例分析
Apr 22 Javascript
javascript 易错知识点实例小结
Apr 25 Javascript
解决vue项目router切换太慢问题
Jul 19 Javascript
jQuery Raty星级评分插件使用方法实例分析
Nov 25 #jQuery
uni-app实现点赞评论功能
Nov 25 #Javascript
js prototype深入理解及应用实例分析
Nov 25 #Javascript
KnockoutJS数组比较算法实例详解
Nov 25 #Javascript
js实现简单的日历显示效果函数示例
Nov 25 #Javascript
VUE.CLI4.0配置多页面入口的实现
Nov 25 #Javascript
用Golang运行JavaScript的实现示例
Nov 25 #Javascript
You might like
linux命令之调试工具strace的深入分析
2013/06/03 PHP
一个基于jquery的图片切换效果
2010/07/06 Javascript
Chosen 基于jquery的选择框插件使用方法
2012/05/30 Javascript
如何制作浮动广告 JavaScript制作浮动广告代码
2012/12/30 Javascript
Jquery中给animation加更多的运作效果实例
2013/09/05 Javascript
javascript简单事件处理和with用法介绍
2013/09/16 Javascript
JavaScript实现多维数组的方法
2013/11/20 Javascript
jquery获取当前点击对象的value方法
2014/02/28 Javascript
Jquery实现侧边栏跟随滚动条固定(兼容IE6)
2014/04/02 Javascript
JQuery中使用ajax传输超大数据的解决方法
2014/07/14 Javascript
node.js中的fs.close方法使用说明
2014/12/17 Javascript
Bootstrap实现响应式导航栏效果
2015/12/28 Javascript
javascript瀑布流布局实现方法详解
2016/02/17 Javascript
JavaScript tab选项卡插件实例代码
2016/02/23 Javascript
超漂亮的Bootstrap 富文本编辑器summernote
2016/04/05 Javascript
实例详解ECMAScript5中新增的Array方法
2016/04/05 Javascript
基于RequireJS和JQuery的模块化编程日常问题解析
2016/04/14 Javascript
jQuery插件FusionCharts绘制ScrollColumn2D图效果示例【附demo源码下载】
2017/03/22 jQuery
p5.js入门教程之鼠标交互的示例
2018/03/16 Javascript
js实现弹窗猜数字游戏
2020/11/26 Javascript
Python中decorator使用实例
2015/04/14 Python
使用Django的模版来配合字符串翻译工作
2015/07/27 Python
Python中pygame安装方法图文详解
2015/11/11 Python
Python实现屏幕截图的两种方式
2018/02/05 Python
python使用turtle绘制国际象棋棋盘
2019/05/23 Python
Python函数中参数是传递值还是引用详解
2019/07/02 Python
Python目录和文件处理总结详解
2019/09/02 Python
用Python做一个久坐提醒小助手的示例代码
2020/02/10 Python
python使用隐式循环快速求和的实现示例
2020/09/11 Python
Python+OpenCV图像处理——实现直线检测
2020/10/23 Python
介绍一下XMLHttpRequest对象的常用方法和属性
2013/05/24 面试题
单位未婚证明范本
2014/01/18 职场文书
解除合同协议书
2014/04/17 职场文书
2015年幼儿园师德师风建设工作总结
2015/10/23 职场文书
慰问信(范文3篇)
2019/10/23 职场文书
浅谈redis整数集为什么不能降级
2021/07/25 Redis