如何优雅地在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中将多个语句写成一个语句的两种方法小结
Dec 08 Javascript
node.js chat程序如何实现Ajax long-polling长链接刷新模式
Mar 13 Javascript
JS中的异常处理方法分享
Dec 22 Javascript
Javascript中常见的校验如域名、手机、邮箱等等
Jan 02 Javascript
$.extend 的一个小问题
Jun 18 Javascript
使用JavaScript获取URL中的参数(两种方法)
Nov 16 Javascript
Bootstrap基本组件学习笔记之缩略图(13)
Dec 08 Javascript
boostrapTable的refresh和refreshOptions区别浅析
Jan 22 Javascript
xmlplus组件设计系列之按钮(2)
Apr 26 Javascript
基于vue+canvas的excel-like组件实例详解
Nov 28 Javascript
微信小程序wx.getUserInfo授权获取用户信息(头像、昵称)的实现
Aug 19 Javascript
JS封装cavans多种滤镜组件
Feb 15 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 结果集的分页实现代码
2009/03/10 PHP
php发送post请求函数分享
2014/03/06 PHP
Javascript 事件流和事件绑定
2009/07/16 Javascript
浅谈node.js中async异步编程
2015/10/22 Javascript
AngularJS表格详解及示例代码
2016/08/17 Javascript
AngularJS中$http服务常用的应用及参数
2016/08/22 Javascript
js正则表达式验证密码强度【推荐】
2017/03/03 Javascript
JavaScript实现设置默认日期范围为最近40天的方法分析
2017/07/12 Javascript
详解React Native顶|底部导航使用小技巧
2017/09/14 Javascript
使用js实现将后台传入的json数据放在前台显示
2018/08/06 Javascript
JavaScript变量提升和严格模式实例分析
2019/01/27 Javascript
Vue侦测相关api的实现方法
2019/05/22 Javascript
vue el-tree 默认展开第一个节点的实现代码
2020/05/15 Javascript
[01:29:17]RNG vs Liquid 2019国际邀请赛淘汰赛 败者组 BO3 第二场 8.23
2019/09/05 DOTA
Python Requests安装与简单运用
2016/04/07 Python
Python正则表达式分组概念与用法详解
2017/06/24 Python
Python遍历某目录下的所有文件夹与文件路径
2018/03/15 Python
浅谈python中np.array的shape( ,)与( ,1)的区别
2018/06/04 Python
对python添加模块路径的三种方法总结
2018/10/16 Python
解决使用PyCharm时无法启动控制台的问题
2019/01/19 Python
Python模块、包(Package)概念与用法分析
2019/05/31 Python
Django Session和Cookie分别实现记住用户登录状态操作
2020/07/02 Python
Python word文本自动化操作实现方法解析
2020/11/05 Python
mac系统下安装pycharm、永久激活、中文汉化详细教程
2020/11/24 Python
python pyg2plot的原理知识点总结
2021/02/28 Python
css3 transform及原生js实现鼠标拖动3D立方体旋转
2016/06/20 HTML / CSS
CSS3贝塞尔曲线示例:创建链接悬停动画效果
2020/11/19 HTML / CSS
shell程序中如何注释
2012/01/28 面试题
商务英语专业自荐信
2013/10/14 职场文书
绿化先进工作者事迹材料
2014/01/30 职场文书
模特大赛策划方案
2014/05/28 职场文书
组工干部对照检查材料
2014/08/25 职场文书
市场营销计划书
2015/01/17 职场文书
大学生村官工作总结2015
2015/04/09 职场文书
springboot临时文件存储目录配置方式
2021/07/01 Java/Android
MySQL中TIMESTAMP类型返回日期时间数据中带有T的解决
2022/12/24 MySQL