如何优雅地在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 相关文章推荐
Extjs学习笔记之二 初识Extjs之Form
Jan 07 Javascript
jquery blockUI 遮罩不能消失与不能提交的解决方法
Sep 17 Javascript
jquery分页插件AmSetPager(自写)
Apr 15 Javascript
探寻Javascript执行效率问题
Nov 12 Javascript
javascript浏览器窗口之间传递数据的方法
Jan 20 Javascript
jquery拖拽效果完整实例(附demo源码下载)
Jan 14 Javascript
BootStrapTable服务器分页实例解析
Dec 20 Javascript
利用transition实现文字上下抖动的效果
Jan 21 Javascript
JS鼠标3次点击事件实现代码及扩展思路
Sep 12 Javascript
mpvue微信小程序开发之实现一个弹幕评论
Nov 24 Javascript
vue-i18n实现中英文切换的方法
Jul 06 Javascript
通过实例解析javascript Date对象属性及方法
Nov 04 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中将汉字转换成拼音的函数代码
2012/09/08 PHP
DEDECMS首页调用图片集里的多张图片
2015/06/05 PHP
浅谈PHP中Stream(流)
2015/06/08 PHP
JS对URL字符串进行编码/解码分析
2008/10/25 Javascript
别了 JavaScript中的isXX系列
2012/08/01 Javascript
javascript教程:关于if简写语句优化的方法
2014/05/17 Javascript
jQuery学习笔记之 Ajax操作篇(二) - 数据传递
2014/06/23 Javascript
推荐25个超炫的jQuery网格插件
2014/11/28 Javascript
javascript实现图片跟随鼠标移动效果的方法
2015/05/13 Javascript
jQuery实现转动随机数抽奖效果的方法
2015/05/21 Javascript
HTML5之WebSocket入门3 -通信模型socket.io
2015/08/21 Javascript
FullCalendar日历插件应用之数据展现(一)
2015/12/23 Javascript
利用jQuery插件imgAreaSelect实现获得选择域的图像信息
2016/12/02 Javascript
JavaScript生成.xls文件的代码
2016/12/22 Javascript
Javascript基础回顾之(三) js面向对象
2017/01/31 Javascript
vue数字类型过滤器的示例代码
2017/09/07 Javascript
vue获取dom元素注意事项
2017/12/28 Javascript
使用async-validator编写Form组件的方法
2018/01/10 Javascript
详解webpack+ES6+Sass搭建多页面应用
2018/11/05 Javascript
Vue插件从封装到发布的完整步骤记录
2019/02/28 Javascript
Vue 使用formData方式向后台发送数据的实现
2019/04/14 Javascript
使用layui前端框架弹出form表单以及提交的示例
2019/10/25 Javascript
使用Promise封装小程序wx.request的实现方法
2019/11/13 Javascript
JS如何在数组指定位置插入元素
2020/03/10 Javascript
Python列表(list)常用操作方法小结
2015/02/02 Python
Python3中编码与解码之Unicode与bytes的讲解
2019/02/28 Python
使用Jupyter notebooks上传文件夹或大量数据到服务器
2020/04/14 Python
三步解决python PermissionError: [WinError 5]拒绝访问的情况
2020/04/22 Python
Python DES加密实现原理及实例解析
2020/07/17 Python
用python批量移动文件
2021/01/14 Python
英国最大的宠物商店:Pets at Home
2019/04/17 全球购物
可口可乐唇膏:Lip Smackers
2019/08/27 全球购物
酒店辞职书怎么写
2015/02/26 职场文书
Redis可视化客户端小结
2021/06/10 Redis
Feign调用传输文件异常的解决
2021/06/24 Java/Android