如何优雅地在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 相关文章推荐
JSON 客户端和服务器端的格式转换
Aug 27 Javascript
理解JavaScript变量作用域更轻松
Oct 25 Javascript
jquery 操作DOM的基本用法分享
Apr 05 Javascript
jQueryUI Datepicker组件设置日期高亮
Oct 13 Javascript
jQuery快速实现商品数量加减的方法
Feb 06 Javascript
微信小程序 常用工具类详解及实例
Feb 15 Javascript
js实现移动端微信页面禁止字体放大
Feb 16 Javascript
老生常谈js-react组件生命周期
May 02 Javascript
javaScript实现滚动条事件详解
Mar 24 Javascript
Vue2.0+Vux搭建一个完整的移动webApp项目的示例
Mar 19 Javascript
微信小程序自定义tabBar在uni-app的适配详解
Sep 30 Javascript
vue内置组件keep-alive事件动态缓存实例
Oct 30 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
解析php根据ip查询所在地区(非常有用,赶集网就用到)
2013/07/01 PHP
PHP中多维数组的foreach遍历示例
2014/06/13 PHP
PHP+MySQL实现输入页码跳转到指定页面功能示例
2018/06/01 PHP
PHP封装XML和JSON格式数据接口操作示例
2019/03/06 PHP
php实现的简单多进程服务器类完整示例
2020/02/01 PHP
jQuery基本选择器选择元素使用介绍
2013/04/18 Javascript
jquery的flexigrid无法显示数据提示获取到数据
2013/07/19 Javascript
简洁Ajax函数处理(示例代码)
2013/11/15 Javascript
JavaScript模拟鼠标右键菜单效果
2020/12/08 Javascript
浅谈Sticky组件的改进实现
2016/03/22 Javascript
JS创建对象的写法示例
2016/11/04 Javascript
Vue中fragment.js使用方法详解
2017/03/09 Javascript
详解使用vue脚手架工具搭建vue-webpack项目
2017/05/10 Javascript
element ui table 增加筛选的方法示例
2018/11/02 Javascript
在React中写一个Animation组件为组件进入和离开加上动画/过度效果
2019/06/24 Javascript
解决layer.open后laydate失效的问题
2019/09/06 Javascript
nuxt 自定义 auth 中间件实现令牌的持久化操作
2020/11/05 Javascript
html+vue.js 实现漂亮分页功能可兼容IE
2020/11/07 Javascript
让python json encode datetime类型
2010/12/28 Python
在Lighttpd服务器中运行Django应用的方法
2015/07/22 Python
Python获取当前公网ip并自动断开宽带连接实例代码
2018/01/12 Python
对python tkinter窗口弹出置顶的方法详解
2019/06/14 Python
python rsync服务器之间文件夹同步脚本
2019/08/29 Python
python定间隔取点(np.linspace)的实现
2019/11/27 Python
Python使用turtle库绘制小猪佩奇(实例代码)
2020/01/16 Python
为什么黑客都用python(123个黑客必备的Python工具)
2020/01/31 Python
python使用隐式循环快速求和的实现示例
2020/09/11 Python
HTML5 Canvas绘制圆点虚线实例
2015/01/01 HTML / CSS
全球采购的街头服饰和帽子:Urban Excess
2020/10/28 全球购物
毕业生护理专业个人求职信范文
2014/01/04 职场文书
应届电子商务毕业自荐书范文
2014/02/11 职场文书
2014年党委工作总结
2014/11/22 职场文书
工会积极分子个人总结
2015/03/03 职场文书
匿名信格式范文
2015/05/27 职场文书
MySQL COUNT函数的使用与优化
2021/05/10 MySQL
Python中的嵌套循环详情
2022/03/23 Python