我们从源码中反推,onerror
实现方式。
Koa错误处理
koa分别从两个地方定义了onerror
方法
application.js
=> app.onerror
源码如下:
onerror(err) {
const isNativeError =
Object.prototype.toString.call(err) === '[object Error]' ||
err instanceof Error;
if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err));
if (404 === err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error(`\n${msg.replace(/^/gm, ' ')}\n`);
}
见词明意
首先判断Error
是否为一个原生的Error对象或者对应继承自Error的实例,如果不是,则会抛出一个TypeError,TypeError表示类型错误,在这里就是指传入的参数err
类型错误。
官方原话:当 err.status
是 404
或 err.expose
是 true
时默认错误处理程序也不会输出错误。
silent
为true也不会将stdout输出到控制台中。
err.stack
,是Error对象中的一个属性,用来保存错误的堆栈信息。
很明显,出现什么错误的时候会调用此方法获取错误信息呢?
源码中有这样一段代码:
if (!this.listenerCount('error')) this.on('error', this.onerror);
Application
继承Emitter
,listenerCount
是NodeJS的Events模块中的方法,用来获取指定监听器的数量,如果没有获取到error
事件(包含插件中error事件,例如:koa-onerror
),则自动调用上面的onerror
方法。
极少数情况下我们会重写app.onerror
方法:
// 错误处理函数
app.onerror = (err, ctx) => {
console.error('server error', err);
};
满足相关条件,会自动调用,自动调用,自动调用
context.js
=> ctx.onerror
核心代码:
onerror(err) {
if (null == err) return;
const isNativeError =
Object.prototype.toString.call(err) === '[object Error]' ||
err instanceof Error;
if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));
...
this.app.emit('error', err, this);
...
const { res } = this;
....
let statusCode = err.status || err.statusCode;
// ENOENT support
if ('ENOENT' === err.code) statusCode = 404;
// default to 500
if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;
// respond
const code = statuses[statusCode];
const msg = err.expose ? err.message : code;
this.status = err.status = statusCode;
this.length = Buffer.byteLength(msg);
res.end(msg);
},
在初始化app实例的时候,已经将app实例挂载到context.app
首先判断err
类型,上面已经提到过。发射emit事件,通过on(下一小节介绍)监听。
从respond
,最终调用res.end
函数 , res是我们熟知response
对象,也就是原生的http.ServerResponse
对象,他的实现过程非常的巧妙,在这里就不再仔细展开。
Node创建原生服务器会使用res.end()
直接关闭连接,msg
错误信息一同会发送到客户端。
那么什么时候会使用此方法返回错误信息呢?
首先可以明确是在自定义的中间件中使用,举个例子:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.onerror(err)
}
});
app.use(async (ctx, next) => {
const order = await getOrderFromDB(ctx.params.id); // 假设这个函数会抛出异常
ctx.body = order;
});
app.on('error', (err) => {
console.error('server error', err);
});
上述第一个中间件,await
等待响应,catch
抛出异常,我们可以想一下,如果说当前中间件产生错误,会造成什么样的影响?
有ctx.onerror(err)
:
上面的源码提到过,ctx.onerror发射了error事件,因此我们控制台可以打印出自定义的错误信息,同时服务因此出现错误
无ctx.onerror(err)
:
控制台并无打印信息
响应404:
未捕获到异常信息,服务会因此崩溃~
因此,他主要是捕获应用程序中未被 try/catch 包围的错误,例如在异步函数或回调函数中抛出的错误,以便能够捕获所有未被处理的错误并进行处理。
Emitter
=> app.on(‘error’)
上面提到过, 一是context.js
发射error
事件,二是application.js
定义全局的onerror
方法,通过on('error')
监听. 它是继承Node中Emitter事件
如果对Node中的底层实现感兴趣,建议大家去读一读 死月老师的 《趣学NodeJS 》 小册
我们可以写个小案例:
const Koa = require('koa');
const jwt = require('jsonwebtoken');
const app = new Koa();
app.secret = 'my_secret_key';
app.on('error', (err, ctx) => {
if (err.name === 'JsonWebTokenError') {
ctx.status = 401;
ctx.body = "无权限";
} else {
ctx.status = 500;
ctx.body = { error: '错误' };
}
});
app.use(async (ctx, next) => {
try {
//获取加密
const token = ctx.headers.authorization.split(' ')[1];
const decoded =await jwt.verify(token, app.secret);
ctx.state.user = decoded;
} catch (err) {
if (err.name == 'JsonWebTokenError') {
console.log('JsonWebTokenError:', err.message);
// throw err;
ctx.app.emit('error', err, ctx);
} else {
throw err;
}
}
});
app.use(async (ctx) => {
ctx.body = { message: 'Hello World!' };
});
app.listen(3000);
用Koa最常见的错误处理方式就是通过emit('error')
和on('error')
处理, 你可以这样理解,自定义错误信息和集中式日志记录等:
const errHandle = (err, ctx) => {
let status, message;
switch (err.message) {
case errTypes.NAME_OR_PASSWORD_IS_REQUIRED:
status = 400; //bad request
message = '用户名或者密码不能为空';
break;
...
break;
default:
status = 404;
message = 'NOT FOUND';
}
ctx.status = status;
ctx.body = message;
};
app.on('error', errHandle);
plugin=> koa-onerror
综合处理错误 中间件,使用方式非常简单, 详解配置可以去npm / githun 去查看相关资料:
const Koa = require('koa');
const onError = require('koa-onerror');
const app = new Koa();
onError(app);
app.use(async (ctx) => {
throw new Error('error');
});
Express错误处理
express源码中对路由的处理部分代码:
if (layerError) {
layer.handle_error(layerError, req, res, next);
} else {
layer.handle_request(req, res, next);
}
router/index.js=> handle_error(err,…args)
在Express源码解析
这篇文章中, 我们未对错误处理的相关源码进行讲解,在这里顺便supply下。
在router/index.js里面对路径替换/匹配完毕后,最终会通过layerError(当前中间件中next传递过来的error)判断执行哪一种中间件调用方式。
此文肯定是执行layer.handle_error
,可见当前方法中有四个参数,源码如下:
Layer.prototype.handle_error = function handle_error(error, req, res, next) {
var fn = this.handle;
if (fn.length !== 4) {
// not a standard error handler
return next(error);
}
try {
fn(error, req, res, next);
} catch (err) {
next(err);
}
};
逻辑很简单,这里就不再解释。
fn(error, req, res, next);
就是我们定义的全局错误中间件:
// error handler
app.use(function (err, req, res, next) {
res.status(err.status || 500);
res.render("error");
});
需要注意的是Express中的err,非Error对象也是可以的
lib/response.js=>sendfile/file.on(‘error’)
源码中还有对某个api特定的错误处理方法:res.sendFile
部分源码如下:
function sendfile(res, file, options, callback) {
var done = false;
var streaming;
function onaborted() {
...
callback(err);
}
// directory
function ondirectory() {
...
callback(err);
}
// errors
function onerror(err) {
...
callback(err);
}
// file
function onfile() {
streaming = false;
}
// finished
function onfinish(err) {
if (err && err.code === 'ECONNRESET') return onaborted();
if (err) return onerror(err);
if (done) return;
setImmediate(function () {
if (streaming !== false && !done) {
onaborted();
return;
}
if (done) return;
done = true;
callback();
});
}
// streaming
function onstream() {
streaming = true;
}
file.on('error', onerror);
...
}
其使用方式如下:
const express = require('express');
const app = express();
app.get('/download', function(req, res) {
const filePath = '/path/to/file';
const options = {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="file.pdf"'
}
};
res.sendFile(filePath, options);
});
sendfile
方法中的第二个参数file,其实是文件系统fs(Node/FileSystem)对象,他内部通过send的库实现的。
因此 file.on('error', onerror);
即:
// 流的方式读取
const reader = fs.createReadStream("./foo.txt", {
start: 0,
end: 10,
highWaterMark: 3
});
// 数据读取的过程
reader.on("data", (data) => {
console.log(data);
reader.pause();
setTimeout(() => {
reader.resume();
}, 1000);
});
未捕获错误处理(扩展)
以Express为例,我们通过app.use((err,...args))
捕获异常错误,但是我们来看个糟糕的例子:
app.get('/getUsers',(res,res) => {
process.nextTick(() => {
throw new Error('delay')
})
})
reader可以流七秒事件想一想,会发生什么?
答案:服务器彻底崩溃,以一种粗暴的形式关闭了整个服务器。
具体原因reader可以在评论区说出你们的idea。
那么我们如何优雅的关闭服务呢?
这里所谓的优雅不拯救,而是做好”投降”之前最后的挣扎。Node中提供了一个全局事件,用于捕获未被处理的异常,即没有被 try-catch
包裹或没有被其他方式处理的异常,上面的demo同理,也就是uncaughtException事件,实现方式如下:
process.on("uncaughtException",err => {
console.log(err)
//在这里做一些必要的清理工作, 例如关闭数据库、redis等。
process.exit(1)
})
重要的提示一下, express中对于未捕获的异常所有仍然使用的
uncaughtException
事件,但是koa则不同,到这里呢~reader们应该有所恍然大悟,第一节代码中,koa中对于未捕获的抛出的错误竟然是404,我那里说崩溃,感觉差强人意 ~。同理在express中你不会再发现有关onerror事件了,统一都是app.use((err,...args))
做处理,也就是说对于未捕获的是没有特定处理的(当然总会有神奇的第三方plugin去解决)。
总结
既是总结也是致谢。给reader们推荐一本关于Express的书籍 《Node与Express开发第二版》,他能让你有意想不到的收获!
Have good ideas, hurry up and practice! Great front -end engineers.
原文链接:https://juejin.cn/post/7214493929566961724 作者:thunderchen