如何优雅地在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 相关文章推荐
javascript中的nextSibling使用陷(da)阱(keng)
May 05 Javascript
JS获得浏览器版本和操作系统版本的例子
May 13 Javascript
js实现YouKu的漂亮搜索框效果
Aug 19 Javascript
JS实现仿Windows7风格的网页右键菜单效果代码
Sep 11 Javascript
深入解析JavaScript编程中的this关键字使用
Nov 09 Javascript
Node.js 文件夹目录结构创建实例代码
Jul 08 Javascript
javascript中使用未定义变量或值的情况分析
Jul 19 Javascript
JS常见创建类的方法小结【工厂方式,构造器方式,原型方式,联合方式等】
Apr 01 Javascript
微信小程序实现全国机场索引列表
Jan 31 Javascript
vue实现微信分享朋友圈,发送朋友的示例讲解
Feb 10 Javascript
JQuery Ajax执行跨域请求数据的解决方案
Dec 10 jQuery
深入理解基于vue-cli的webpack打包优化实践及探索
Oct 14 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
Protoss兵种介绍
2020/03/14 星际争霸
真正的ZIP文件操作类(php)
2007/07/21 PHP
PHP+jQuery实现自动补全功能源码
2013/05/15 PHP
php判断GIF图片是否为动画的方法
2020/09/04 PHP
PHP中new static()与new self()的区别异同分析
2014/08/22 PHP
php post换行的方法
2020/02/03 PHP
jQuery ui1.7 dialog只能弹出一次问题
2009/08/27 Javascript
你必须知道的JavaScript 变量命名规则详解
2013/05/07 Javascript
JS、DOM和JQuery之间的关系示例分析
2014/04/09 Javascript
超级简单的jquery操作表格方法
2014/12/15 Javascript
使用npm发布Node.JS程序包教程
2015/03/02 Javascript
初步认识JavaScript函数库jQuery
2015/06/18 Javascript
Bootstrap入门书籍之(四)菜单、按钮及导航
2016/02/17 Javascript
AngularJS 过滤器(自带和自建)详解
2016/09/19 Javascript
jQuery插件zTree实现删除树子节点的方法示例
2017/03/08 Javascript
详解Vue结合后台的列表增删改案例
2018/08/21 Javascript
jQuery+ajax实现文件上传功能
2020/12/22 jQuery
[02:16]完美世界DOTA2联赛PWL S3 集锦第三期
2020/12/21 DOTA
python实现通过代理服务器访问远程url的方法
2015/04/29 Python
详解Django中的权限和组以及消息
2015/07/23 Python
Python实现二叉树结构与进行二叉树遍历的方法详解
2016/05/24 Python
python strip() 函数和 split() 函数的详解及实例
2017/02/03 Python
Python探索之自定义实现线程池
2017/10/27 Python
django框架自定义模板标签(template tag)操作示例
2019/06/24 Python
在python3中实现更新界面
2020/02/21 Python
django修改models重建数据库的操作
2020/03/31 Python
Python中常见的数制转换有哪些
2020/05/27 Python
Python如何读写字节数据
2020/08/05 Python
CSS3 创建网页动画实现弹跳球动效果
2018/10/30 HTML / CSS
简约控的天堂:The Undone
2016/12/21 全球购物
飞利浦西班牙官方网站:Philips西班牙
2020/02/17 全球购物
职专应届生求职信
2013/11/16 职场文书
陈欧的广告词
2014/03/18 职场文书
英语四级考试作弊检讨书
2014/09/29 职场文书
使用@Value值注入及配置文件组件扫描
2021/07/09 Java/Android
USB TYPE-C 或将成为所有智能手机充电标准
2022/04/21 数码科技