Node.js 中的错误处理和常用调试方法

Error 类

1. Error 分类
  • Error : 通用的错误类型,如: new Error(‘error!!!’)
  • SyntaxError : 语法错误,如: require(‘vm’).runInThisContext(‘binary ! isNotOk’)
  • ReferenceError : 引用错误,如引用一个未定义的变量,如: doesNotExist
  • TypeError : 类型错误,如: require(‘url’).parse(() => { });
  • URIError : 全局的 URI 处理函数抛出的错误,如: encodeURI(’\uD800’)
  • AssertError : 使用 assert 模块时抛出的错误,如: assert(false)
2. Error 对象

new Error(message) 会生成一个 Error 对象

  • error.code:定义 error 的类型,系统抛出的异常会有相关 code 定义,用户有需求,要自己给 error 设置code
  • error.message:消息错误描述
  • error.stack:错误堆栈跟踪信息
3. Error.captureStackTrace(targetObject[, constructorOpt])

该函数捕获 targetObject 调用栈,只能使用在 V8 引擎上:

//captureStackTrace 会给 targetObject 设置 stack 属性,用于获取 targetObject 的调用栈信息
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack;  // similar to `new Error().stack`

与 new Error().stack 的区别:

  1. 无需new一个新的Error对象,节省内存空间,同时代码上也会更加优雅。
  2. 如果使用Error.captureStackTrace,则对于堆栈信息的格式化工作会被延迟至访问targetObj.stack时进行,如果targetObj.stack未被访问,则堆栈信息的格式化工作会被省略,从而节省计算资源
4. Error.prepareStackTrace(error, structuredStackTrace)
  • 该方法可以定制 Error 对象的 stack 信息
  • 第一个参数error对象本身,第二个参数是调用栈信息数组
  • Error 默认情况下返回的 stack 信息是一个字符串,我们可以通过该函数进行定制化处理,格式化等操作
  • 这个方法是 V8 暴露出来的,所以只能在基于 V8 的 Node.js 或者 Chrome 里才能使用。
  • 这个方法会修改全局 Error 的行为,所以如果局部修改 Error 行为的话,需要先把原先的 Error.prepareStackTrace,方便复原默认行为
  • callSite是一个对象,其中包括很多有用的函数如下:
- getThis : returns the value of this
- getTypeName : returns the type of this as a string. This is the name of the function stored in the constructor field of this, if available, otherwise the - - - - object's [[Class]]internal property.
- getFunction : returns the current function
- getFunctionName : returns the name of the current function, typically its name property. If a name property is not available an attempt will be made to try to infer a name from the function's context.
- getMethodName : returns the name of the property of this or one of its prototypes that holds the current function
- getFileName : if this function was defined in a script returns the name of the script
- getLineNumber : if this function was defined in a script returns the current line number
- getColumnNumber : if this function was defined in a script returns the current column number
- getEvalOrigin : if this function was created using a call to eval returns a CallSite object representing the location where eval was called
- isToplevel : is this a toplevel invocation, that is, is this the global object?
- isEval : does this call take place in code defined by a call to eval?
- isNative : is this call in native V8 code?
- isConstructor : is this a constructor call?
// 报错默认行为
let oldPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = function (error, callSites) {
  return error.toString() + '\n' + callSites.map(callSite => {
    return '    -> ' + callSite.getFunctionName() + ' ('
      + callSite.getFileName() + ':'
      + callSite.getLineNumber() + ':'
      + callSite.getColumnNumber() + ')'
  }).join('\n')
}
/*do something*/
// 复原默认行为
Error.prepareStackTrace = oldPrepareStackTrace;
5. Error.stackTraceLimit

可以修改堆栈信息的深度,默认为10。

6. Long Stack Trace

Node.js 中有很多异步操作,问题出在异步操作,当遇到异步回调就会丢失绑定回调前的调用栈信息:

//这段代码就会丢失 bar 函数的调用栈
var foo = function () {
  throw new Error('error!!!')
}
var bar = function () {
  setTimeout(foo)
}
bar()

/Users/nswbmw/Desktop/test/app.js:2
  throw new Error('error!!!')
  ^

Error: error!!!
    at Timeout.foo [as _onTimeout] (/Users/nswbmw/Desktop/test/app.js:2:9)
    at ontimeout (timers.js:469:11)
    at tryOnTimeout (timers.js:304:5)
    at Timer.listOnTimeout (timers.js:264:5)

不过 Node 8 提供了 async_hooks 模块,该模块可以追踪异步的调用路径,想了解该模块可以看我的博客-学习使用 NodeJs 中 async-hooks 模块。

错误处理

错误分类
错误 名称 触发
Standard JavaScript errors 标准 JavaScript 错误 由错误代码触发
System errors 系统错误 由操作系统触发
User-specified errors 用户自定义错误 通过 throw 抛出
Assertion errors 断言错误 assert 模块触发
错误处理方法

在 Node.js 中错误处理主要有以下几种方法:

  • callback(err, data) 回调约定,错误优先的 callback 仅仅是一种约定并没有强制性
readFile('./a.txt', function(err, data){
   if(err){
       console.log('callback.error', err);
   } 
});

  • throw / try / catch,对于同步异常处理
try{
    /*do something*/
    throw new Error('throwError')
}catch(err){
    console.log('throwerr:', err);
}

  • EventEmitter 的 error 事件
const myEmitter = new MyEmitter();
myEmitter.on('error', (err) => {
  console.error('whoops! there was an error');
});
myEmitter.emit('emitter.error', new Error('whoops!'));
  • Promise
new Promise.reject('a error').catch(err => {console.log('promise.error:', err)});
  • co, async 等封装实现
let f = async function(){
    try {
        await doPromise();
    }catch(err){
        console.log('await.error:', err);
    }
}

domain

domain 本身是一个 EventEmitter 对象, 其中文意思是 “域” 的意思, 捕获异步异常的基本思路是创建一个域, cb 函数会在定义时会继承上一层的域, 报错通过当前域的.emit(‘error’, err) 方法触发错误事件将错误传递上去, 从而使得异步错误可以被强制捕获:

//引入一个domain的中间件,将每一个请求都包裹在一个独立的domain中
//domain来处理异常
app.use(function (req,res, next) {
  var d = domain.create();
  //监听domain的错误事件
  d.on('error', function (err) {
    logger.error(err);
    res.statusCode = 500;
    res.json({sucess:false, messag: '服务器异常'});
    d.dispose();
  });
  
  d.add(req);
  d.add(res);
  d.run(next);
});

但是 domain 的引入也带来了更多新的问题. 比如依赖的模块无法继承你定义的 domain, 导致你写的 domain 无法 cover 依赖模块报错. 而且, 很多人 (特别是新人) 由于不了解 Node.js 的内存/异步流程等问题, 在使用 domain 处理报错的时候, 没有做到完善的处理并盲目的让代码继续走下去, 这很可能导致项目完全无法维护,目前该模块已经被废弃

uncaughtException/unhandledRejection/rejectionHandled 事件

uncaughtException/unhandledRejection/rejectionHandled 是 process 进程对象可以监听的几个异常处理的事件:

  • uncaughtException: 当异常没有被捕获一路冒泡到 Event Loop 时就会触发该事件

应尽量避免使用 uncaughtException 事件来处理错误,官方建议的使用 uncaughtException 的正确姿势是在结束进程前使用同步的方式清理已使用的资源 (文件描述符、句柄等) 然后 process.exit

  • unhandledRejection:监听一个当前事件循环中,未被捕获的异常,该异常可能在之后的循环中被捕获
  • rejectionHandled:监听一个Rejected Promise在事件循环的下次轮询或者之后被绑定了一个异常处理函数(catch)时触发
var p = (new Promise(function(resolve, reject){
    reject(new Error('首次抛出异步异常'));
    // 或者通过 throw 的方式抛出,效果相同
    // throw new Error('Error from promise by throw');

}));

//监听未捕获的同步异常事件
process.on('uncaughtException', function(e){
    console.error('uncaughtException:Catch in process', e.message);
});

//监听一个当前事件循环中,未被捕获的异常,该异常可能在之后的循环中被捕获
process.on('unhandledRejection', (reason) => {
    console.info('unhandledRejection:Catch in process', reason.message);
});

//监听一个Rejected Promise在事件循环的下次轮询或者之后被绑定了一个异常处理函数(catch)时触发
process.on('rejectionHandled', (p) => {
    console.info('rejectionHandled:Catch in process', p);
});

setTimeout(function(){
    p.catch(function(e){
        //console.error('Catch in Promise', e);
        throw new Error('再次抛出异步异常');
    });
}, 3e3);

(function(){
    throw new Error('同步异常');
})()

//上述代码的输出结果为:

/*uncaughtException:Catch in process 同步异常
unhandledRejection:Catch in process 首次抛出异步异常
rejectionHandled:Catch in process Promise {
   Error: 首次抛出异步异常
    at /Users/zhangdianpeng/projectCode/myProject/MyError/index.js:8:12
    at Promise ()
    at Object. (/Users/zhangdianpeng/projectCode/myProject/MyError/index.js:7:10)
    at Module._compile (module.js:573:30)
    at Object.Module._extensions..js (module.js:584:10)
    at Module.load (module.js:507:32)
    at tryModuleLoad (module.js:470:12)
    at Function.Module._load (module.js:462:3)
    at Function.Module.runMain (module.js:609:10)
    at startup (bootstrap_node.js:158:16) }
unhandledRejection:Catch in process 再次抛出异步异常*/

常用调试方法

1. 日志打印方式

这种方法平时检测一下还是可以的,真正有难缠的bug的时候会有种暴力穷举的感觉,常用的方法如下:

console 模块

该模块导出了两个特定的组件:

  • 一个 Console 类,包含 console.log() 、 console.error() 和 console.warn() 等方法,可以被用于写入到任何 Node.js 流。
  • 一个全局的 console 实例,可被用于写入到 process.stdout 和 process.stderr。 全局的 console 使用时无需调用 require(‘console’)
  • 全局的 console 是 Console 的一个特例,相当于调用 new Console(process.stdout, process.stderr);
  • 全局的 console 对象的方法既不总是同步的(如浏览器中类似的 API),也不总是异步的(如其他 Node.js 流)
//全局Console的使用方式:
const output = fs.createWriteStream('./stdout.log');
const errorOutput = fs.createWriteStream('./stderr.log');
// 自定义的简单记录器
const logger = new Console(output, errorOutput);
// 像 console 一样使用
const count = 5;
logger.log('count: %d', count);

console提供的方法:

  • console.log/console.info
  • console.error/console.warn
  • console.clear(),与 shell 的 clear 命令类似,清空 tty 终端
  • console.dir(obj {depth: null, colors: true}),在 obj 上使用 util.inspect() 并打印结果字符串到 stdout, depth 如果是 null 的话,不限制打印深度
  • console.time(label)/console.timeEnd(label) 根据label启动定时器,并打印消耗的时间
asserts 模块

用于调试断言的模块,常用函数:

  • 相等判断:
let assert = require('assert');
assert(1 === 1, 'is not true'); 
assert.ok(1 === 1, 'is not true'); // assert() 是 assert.ok 的别名
assert.equal(1, 1, 'is not true')
assert.deepStrictEqual({a: {b: 1}}, {a: {b: 1}}, 'is not ok') // 深度相等判断,原始值使用相等运算符(==)比较, 而对象只测试自身可枚举的属性
  • doesNotThrow(fun, error, message) => 断言 block 函数不会抛出错误,如果抛出异常且和error的类型不一致,将报错
assert.doesNotThrow(
  () => {
    throw new TypeError('错误值');
  },
  TypeError
);
  • assert.ifError(value) => 可用于测试回调函数的 error 参数
  • assert.notEqual => 与assert.equal相反
  • assert.throws => 断言函数会抛出异常
debug 模块

debug 是一个小巧却非常实用的日志模块,可以根据环境变量决定打印不同类型(或级别)的日志,代码如下:

const normalLog = require('debug')('log')
const errorLowLog = require('debug')('error:low')
const errorNormalLog = require('debug')('error:normal')
const errorHighLog = require('debug')('error:high')

setInterval(() => {
  const value = Math.random()
  switch (true) {
    case value < 0.5: normalLog(value); break
    case value >= 0.5 && value < 0.7: errorLowLog(value); break
    case value >= 0.7 && value < 0.9: errorNormalLog(value); break
    case value >= 0.9: errorHighLog(value); break
    default: normalLog(value)
  }
}, 1000)

在启动的 node 进程时指定DEBUG环境变量便可以打印相应等级的日志:

DEBUG=*:打印所有类型的日志
DEBUG=log:只打印 log 类型的日志
DEBUG=error:*:打印所有以 error: 开头的日志
DEBUG=error:*,-error:low:打印所有以 error: 开头的并且过滤掉 error:low 类型的日志,即这里只打印一般级别和严重级别的错误日志
repl2 模块

一个增强版的 repl,我们有时候想测试某个系统函数或者第三方函数的功能时,或者在 repl 里运行一些测试代码时,这个库很有必要。repl2 会根据一个用户配置(~/.noderc),预先加载模块到 REPL 中,省下了我们手动在原生 REPL 中 require 模块的过程:

  • 添加配置到 ~/.noderc:
{
  "lodash": "__",
  "moment": "moment",
  "validator": "validator"
}
  • 运行 noder:
$ noder
> moment().format('YYYY')
'2017'
> __.random(0, 5)
3
> validator.isEmail('[email protected]')
true

repl2 的源码实现也特别简单易懂,具体想了解如何实现,可以看的博客 Nodejs 定制化你自己的REPL

power-assert 模块

以“No API is the best API”为准则,可以实现无缝替换官方 assert 模块,是目前最好用的断言库

相比其它断言库,最大的优点就是错误信息非常直观:

various types demo:

      AssertionError:   # test/example2.js:43

  assert(types[index].name === bob.name)
         |    ||      |    |   |   |
         |    ||      |    |   |   "bob"
         |    ||      |    |   Person{name:"bob",age:5}
         |    ||      |    false
         |    |11     "alice"
         |    Person{name:"alice",age:3}
         ["string",98.6,true,false,null,undefined,#Array#,#Object#,NaN,Infinity,/^not/,#Person#]

  --- [string] bob.name
  +++ [string] types[index].name

想了解更多,可以参考这篇博客-可能是最好的 JS Assert 库 - 皇帝的新衣

2. node 自带的 debugger 调试器

启动 node 脚本带上 inspect 参数可以进入命令行调试界面,常用操作如下:

  • 相关步进调试命令
* `cont`, `c`  - 继续执行
* `next`, `n`  - 下一步
* `step`, `s`  - 跳进函数
* `out`, `o`   - 跳出函数
* `pause`      - 暂停运行代码(类似开发者工具中的暂停按钮)
  • 设置断点

断点设置可以在代码中用debugger;设置,也可以在调试的过程中设置:

* `setBreakpoint()`, `sb()`                     - 在当前行设置断点
* `setBreakpoint(line)`, `sb(line)`             - 在指定行设置断点
* `setBreakpoint('fn()')`, `sb(...)`            - 在函数体的第一条语句设置断点
* `setBreakpoint('script.js', 1)`, `sb(...)`    - 在 script.js 的第 1 行设置断点
* `clearBreakpoint('script.js', 1)`, `cb(...)`  - 清除 script.js 第 1 行的断点
  • 获取其它信息
* `backtrace`, `bt`     - 打印当前执行框架的回溯
* `list(5)`             - 列出脚本源代码的 5 行上下文(前后各 5 行)
* `watch(expr)`         - 添加表达式到监视列表
* `unwatch(expr)`       - 从监视列表移除表达式
* `watchers`            - 列出所有监视器和它们的值(每个断点会自动列出),可以监听设置好的参数的值
* `repl`                - 打开调试器的 repl,用于在所调试的脚本的上下文中进行执行,在repl中可以直接运行命令,获取值
* `exec expr`           - 在所调试的脚本的上下文中执行一个表达式,相当于在repl中运行命令一样

3. 使用 --inspect 和 chrome develop tool 相结合调试

node --inspect app.js

  • 默认监听 9229 端口,ws://127.0.0.1:9229
  • 可以使用node --inspect=9223 app.js 改变监听端口
  • 具体监听的 IP 和端口可以在 chrome://inspect/#devices 下配置
  • 打开浏览器的开发者工具,点击左上角的 open the devTools for NodeJs 便可以进入 nodejs 调试界面
  • 浏览器上直接映射服务端代码,和前端调试类似

node --inspect-brk app.js 默认在第一行代码就加上断点

4. 使用 node-inspector 工具进行调试

具体看文档


npm install node-inspector -g

node-debug app.js

5. 使用 IDE 作调试

基本上每个 IDE 都集成了对应的调试入口

参考文献

  • 错误处理与调试
  • 错误处理的最佳实践
  • node-verror
  • Node.js 异步异常的处理与domain模块解析
  • Stack Trace API
  • Node.js 调试指南 - Error Stack
  • Node.js 性能调优之代码篇(二)——错误栈
  • 可能是最好的 JS Assert 库 - 皇帝的新衣
  • Nodejs 定制化你自己的REPL

你可能感兴趣的:(Nodejs学习之路,Node.js异常处理,断言,调试,debug)