框架文章阅读
得益于框架支持的异步编程模型,错误完全可以用 try catch
来捕获。在编写应用代码时,所有地方都可以直接用 try catch
来捕获异常。
按照正常代码写法,所有的异常都可以用这个方式进行捕获并处理,但是一定要注意一些特殊的写法可能带来的问题。打一个不太正式的比方,我们的代码全部都在一个异步调用链上,所有的异步操作都通过 await 串接起来了,但是只要有一个地方跳出了异步调用链,异常就捕获不到了。
如果 service.trade.check
方法中代码有问题,导致执行时抛出了异常,尽管框架会在最外层通过 try catch
统一捕获错误,但是由于 setImmediate
中的代码『跳出』了异步链,它里面的错误就无法被捕捉到了。因此在编写类似代码的时候一定要注意。
框架也考虑到了这类场景,提供了 ctx.runInBackground(scope)
辅助方法,通过它又包装了一个异步链,所有在这个 scope 里面的错误都会统一捕获。
class HomeController extends Controller {
async buy () {
const request = {};
const config = await ctx.service.trade.buy(request);
// 下单后需要进行一次核对,且不阻塞当前请求
ctx.runInBackground(async () => {
// 这里面的异常都会统统被 Backgroud 捕获掉,并打印错误日志
await ctx.service.trade.check(request);
});
}
}
了保证异常可追踪,必须保证所有抛出的异常都是 Error 类型,因为只有 Error 类型才会带上堆栈信息,定位到问题。
框架通过 onerror 插件提供了统一的错误处理机制。对一个请求的所有处理方法(Middleware、Controller、Service)中抛出的任何异常都会被它捕获,并自动根据请求想要获取的类型返回不同类型的错误(基于 Content Negotiation)。
onerror 插件的配置中支持 errorPageUrl 属性,当配置了 errorPageUrl 时,一旦用户请求线上应用的 HTML 页面异常,就会重定向到这个地址。
请求需求的格式 | 环境 | errorPageUrl 是否配置 | 返回内容 |
---|---|---|---|
HTML & TEXT | local & unittest | - | onerror 自带的错误页面,展示详细的错误信息 |
HTML & TEXT | 其他 | 是 | 重定向到 errorPageUrl |
HTML & TEXT | 其他 | 否 | onerror 自带的没有错误信息的简单错误页(不推荐) |
JSON & JSONP | local & unittest | - | JSON 对象或对应的 JSONP 格式响应,带详细的错误信息 |
JSON & JSONP | 其他 | - | JSON 对象或对应的 JSONP 格式响应,不带详细的错误信息 |
// config/config.default.js
module.exports = {
onerror: {
// 线上页面发生异常时,重定向到这个页面上
errorPageUrl: '/50x.html',
},
};
尽管框架提供了默认的统一异常处理机制,但是应用开发中经常需要对异常时的响应做自定义,特别是在做一些接口开发的时候。框架自带的 onerror 插件支持自定义配置错误处理方法,可以覆盖默认的错误处理方法。
✋404
框架并不会将服务端返回的 404 状态当做异常来处理,但是框架提供了当响应为 404 且没有返回 body 时的默认响应。
当请求被框架判定为需要 JSON 格式的响应时,会返回一段 JSON:
{ "message": "Not Found" }
当请求被框架判定为需要 HTML 格式的响应时,会返回一段 HTML:
<h1>404 Not Foundh1>
但是能够支持配置,将默认的 HTML 请求的 404 响应重定向到指定的页面。
// config/config.default.js
module.exports = {
notfound: {
pageUrl: '/404.html',
},
};
自定义响应404
// app/middleware/notfound_handler.js 中间件
module.exports = () => {
return async function notFoundHandler(ctx, next) {
await next();
if (ctx.status === 404 && !ctx.body) {
if (ctx.acceptJSON) {
ctx.body = { error: 'Not Found' };
} else {
ctx.body = 'Page Not Found
';
}
}
};
};
配置中间件:
// config/config.default.js
module.exports = {
middleware: [ 'notfoundHandler' ],
};
统一错误处理——中间件的形式
Controller 和 Service 都有可能抛出异常,这也是我们推荐的编码方式,当发现客户端参数传递错误或者调用后端服务异常时,通过抛出异常的方式来进行中断。
this.ctx.validate()
进行参数校验,失败抛出异常。this.ctx.curl()
方法访问 CNode 服务,可能由于网络问题等原因抛出服务端异常。在 app/middleware
目录下新建一个 error_handler.js
的文件来新建一个 middleware
// app/middleware/error_handler.js
module.exports = () => {
return async function errorHandler(ctx, next) {
try {
await next();
} catch (err) {
// 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志
ctx.app.emit('error', err, ctx);
const status = err.status || 500;
// 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
const error = status === 500 && ctx.app.config.env === 'prod'
? 'Internal Server Error'
: err.message;
// 从 error 对象上读出各个属性,设置到响应中
ctx.body = { error };
if (status === 422) {
ctx.body.detail = err.errors;
}
ctx.status = status;
}
};
};
加载中间件config/config.default.js
// config/config.default.js
module.exports = {
// 加载 errorHandler 中间件
middleware: [ 'errorHandler' ],
// 只对 /api 前缀的 url 路径生效
errorHandler: {
match: '/api',
},
};
对error的类型进行判断,返回自定义的message
throwBizErr
这类的方式去处理。应用自定义
定制特殊响应的功能,而不是通过 302 跳转到其他地方。
兼容性的考虑
notfound throw 404 error
框架和应用都可以覆盖 app/onerror.js
来实现统一处理逻辑。
// app/onerror.js
module.exports = {
'404': {
* html(ctx, err) {
// 这里可以使用 render
yield ctx.render('404.html');
},
* json(ctx, err) {
// 不处理或者不配置或者返回 null, undefined,都会使用默认的 json 逻辑来处理
},
},
'403': function* (ctx, err) {
// all 的精简版本写法
},
'4xx': {
* all(ctx, err) {
// all 不区分 accepts,由开发者自行处理
},
},
};
错误分为三种未捕获异常、系统异常、业务异常,以下是分类比较
定义 | 未捕获异常 | 系统异常 | 业务错误 |
---|---|---|---|
类名 | Error | xxxException | xxxBizError |
说明 | js 内置错误,未做任何处理 | 自己抛出的系统异常 | 自己抛出的业务异常 |
错误处理方 | 由 onerror 插件处理 | 业务可扩展处理 | 业务可扩展处理 |
可识别 | 否 | 是 | 是 |
属性扩展 | 否 | 是 | 是 |
所有的类均继承自Error类,并定义BaseError类,继承自 BaseError 的错误是可以被识别的,而其他三方继承 Error 的类都无法被识别。
类名只是用来区分三种错误,继承可以自定义
业务错误处理封装成插件,比如egg-bizerror:
npm上的解释:
usage:
// config/plugin.js
exports.bizerror = {
enable: true,
package: 'egg-bizerror',
};
// config/config.default.js
exports.bizerror = {
breakDefault: false, // disable default error handler禁用默认错误处理
sendClientAllParams: false, // return error bizParams to user,返回错误参数给用户
interceptAllError: false, // handle all exception, not only bizError exception处理所有的异常,不仅是业务异常。
};
// config/errorcode.js
module.exports = {
'USER_NOT_EXIST': {
status: 400,
code: '400' // override code value,覆盖code value。
message: 'can`t find user info',
errorPageUrl: '', // app will redirect this url when accepts is html
addtion1: 'a', // any, will return to browser 附件性的
},
'NOT_FOUND': {
errorPageUrl: (ctx, error) => {
return '/404.html';
}
}
'404': (ctx, error) => {
ctx.redirect('/404.html');
return false; // you can return false, break default logic,打断默认的逻辑
}
}
API:
ctx.throwBizError(code, error, bizParams)–业务逻辑
throw an biz error
error.code
, default SYSTEM_EXCEPTION
, read errorcode config with this value when handle error.Error
object.error.bizParams
, extra data, can help you solve the problem.bizParams还有下面的三个参数:
errors
of json object and send to client.error.code
.error.log
, if false, not log this error, defalut true.// throw an error object
// error.code
// error.message
// error.log
// error.bizParams
// error.bizError
ctx.throwBizError('system_exception')
ctx.throwBizError(new Error())
ctx.throwBizError({ code: 'system_exception', log: false })
ctx.throwBizError('system_exception', { userId: 1, log: false })
ctx.throwBizError('system_exception', 'error message')
ctx.throwBizError('system_exception', new Error())
ctx.throwBizError(new Error(), { userId: 1, log: false })
ctx.throwBizError('system_exception', 'error message', { userId: 1, log: false })
try {
this.ctx.body = {
data: this.ctx.request.body,
};
throw new Error('hahahah');
} catch (error) {
this.ctx.throwBizError({ code: '-9999', userId: 1, log: false });
}
的結果是:
{"code":"-9999","message":"System Exception","errors":{"userId":1}}
ctx.responseBizError(error, bizParams)—返回响应
handle the error
bizError: true
, otherwise, the plugin will throw this error.第三种调用方法:
app.on(‘responseBizError’, (ctx, error) => {})
you can add listener to do some thing.
第四种重写:
example:
// app/service/user.js
module.exports = app => {
class User extends app.Service {
async getUserId() {
let userInfo;
try {
userInfo = await this.getUser();
} catch (error) {
ctx.responseBizError(error, { bizError: true, code: 'USER_NOT_EXIST' })
return;
}
if (!userInfo || !userInfo.id) {
ctx.throwBizError('USER_NOT_EXIST');
}
return userInfo.id;
}
}
return User;
};
// app.js
// add handle logic
module.exports = app => {
app.on('responseBizError', (ctx, error) => {
if (error.bizParams && error.bizParams.bizType === 'getUser') {
errorCount++;
}
});
};
// app.js
// override default handler
module.exports = app => {
app.BizErrorHandler = class extends app.BizErrorHandler {
json(ctx, error, config) {
ctx.body = {
code: config.code,
msg: config.message,
};
}
}
};
egg-onerror
默认在egg框架中。 但是你仍旧需要设置选项来匹配你的场景。.
errorPageUrl: String or Function
- 如果用户请求html页面在生产环境上,并抛出了未捕获的错误,他将定向到错误页面 errorPageUrl
.accepts: Function
- 检测用户的请求 json
or html
.all: Function
- 定制错误处理器 如果 all
存在, 其他的将被忽略.html: Function
- 定制html错误处理器.text: Function
- 定制text错误处理器.json: Function
- 定制json错误处理器.jsonp: Function
- 定制jsonp错误处理器./ config.default.js
// errorPageUrl support funtion
exports.onerror = {
errorPageUrl: (err, ctx) => ctx.errorPageUrl || '/500',
};
// an accept detect function that mark all request with `x-requested-with=XMLHttpRequest` header accepts json.
function accepts(ctx) {
if (ctx.get('x-requested-with') === 'XMLHttpRequest') return 'json';
return 'html';
}
一般性错误处理的原则:
现在的错误处理插件是 egg-onerror,但这个插件主要是优雅处理未捕获异常,也就是了为了让应用不挂进行兜底,但是现在没有一种统一的业务错误处理方案。
业务校验:
比如参数校验、业务验证等等,这些并不属于异常,一般会在响应时转成对应的数据格式。常见的处理方式是接口返回错误,并在 response 转换
class User extends Controller {
async show() {
const error = this.check(this.params.id);
if (error) {
this.ctx.status = 422;
this.ctx.body {
message: error.message,
};
return;
}
// 继续处理
}
check(id) {
if (!id) return { message: 'id is required' };
}
}
异常类型的区分:
将已知异常和未捕获异常做差异化处理。
例如状态码未捕获时返回500,已知异常需要返回422等~
标准化响应:
如果业务抛出自定义的系统异常和业务错误,可直接在错误处理里面处理,未捕获异常在 onerror 中处理。
继承的错误可增加额外属性,比如 HttpError 可增加 status 属性作为处理函数的输入。
字段:
name: 一般为类名,如 NotFoundError
message: 错误的具体信息,可读的,如 404 Not Found
code: 大写的字符串,描述错误,如 NOT_FOUND
status: http 状态码,400
错误处理是最核心的功能,有如下规则
errors for eggjs
提供两种类型的错误:错误,异常
创建Error
const { EggError, EggException } = require('egg-errors');
let err = new EggError('egg error');
console.log(EggError.getType(err)); // ERROR
创建Exception
err = new EggException('egg exception');
console.log(EggException.getType(err)); // EXCEPTION
也能引入一个错误从普通的错误对象
err = new Error('normal error');
console.log(EggError.getType(err)); // BUILTIN
err = EggError.from(err);
console.log(EggError.getType(err)); // ERROR
错误也能被扩展:
const { EggBaseError } = require('egg-errors');
class CustomError extends EggBaseError {
constructor(message) {
super({ message, code: 'CUSTOM_CODE' });
}
}
或者使用ts能够扩展错误选项:
import { EggBaseError, ErrorOptions } from 'egg-errors';
class CustomErrorOptions extends ErrorOptions {
public data: object;
}
class CustomError extends EggBaseError<CustomErrorOptions> {
public data: object;
protected options: CustomErrorOptions;
constructor(options?: CustomErrorOptions) {
super(options);
this.data = this.options.data;
}
}
建议使用message代替options在用户的地方,他能够很简单的被开发者理解。
HTTP错误,是固有的错误,转变成400~500状态码的错误对象,HTTPError扩展EggBaseError提供了两个,status
和headers
.
const { ForbiddenError } = require('egg-errors');
const err = new ForbiddenError('your request is forbidden');
console.log(err.status); // 403
可获得的错误:
BaseError
|- EggBaseError
| |- EggError
| |- HttpError
| | |- NotFoundError
| | `- ...
| `- CustomError
`- EggBaseException
|- EggException
`- CustomException
前置资料:
所有由egg和egg插件以前的已知的error异常,都需要规范err.code。
意见建议稿:
const errors = require('egg').errors;
const err = new errors.TypeError('ERR_EGG_SOME_ERROR_CODE_STRING', 'Some error haha');
console.log(err.code); // 'ERR_EGG_SOME_ERROR_CODE_STRING'
const errors = require('egg').errors;
// 使用'ERR_EGG_MY_ERROR'注册一个新的子类
// 如果'ERR_EGG_MY_ERROR'存在, 将抛出一个类型错误,错误码是 'ERR_EGG_DUPLICATE_CODE'
errors.E('ERR_EGG_MY_ERROR', TypeError);
const err = new errors.ERR_EGG_MY_ERROR('my error here');
console.log(err.name); // 'TypeError'
console.log(err.code); // 'ERR_EGG_MY_ERROR'
message & code✋
错误包含两个信息,message&code,message是能够改变的,但是他不将有大的改变,它有补丁或者微小的变化,代码首次出现后应保持稳定。
天猪大佬的话:
koa context包装了request和response对象在,并提供了非常有用的方法来写api和web应用。这些操作疆场用在HTTP服务开发,在这个级别上添加而不是更高级别的框架,迫使中间件执行这个普通的函数。
一个context创建每个request,在中间件中医用座位接收者或者ctx识别,示例:
app.use(async ctx => {
ctx; // is the Context
ctx.request; // is a Koa Request
ctx.response; // is a Koa Response
});
很多context的存取和方法都可表示为ctx.request
orctx.response
,为了使用起来方便,是相似的。ctx.type
&ctx.length
代表着response
,ctx.path
&ctx.method
代表着request
ctx.req
ctx.res
避免使用node中的属性:
res.statusCode
res.writeHead()
res.write()
res.end()
ctx.request
&ctx.response
是koa中的object
ctx.state
推荐的命名空间,通过中间件和前端视图来进行传递信息。
ctx.state.user = await User.find(id);
ctx.app
ctx.cookies.get(name[,options])
signed
cookie应该被签名
koa使用cookie模块选项简单通过。
ctx.cookies.set(name,value,[options])
设置cookie得键值对的选项:
maxAge
以毫秒为单位设置过期时间signed
签名cookie的值expires
一个日期为cookie的满期path
cookie path, /'
by defaultdomain
cookie域secure
secure cookiehttpOnly
server-accessible cookie, true by defaultoverwrite
一个boolean值,是否覆盖以前设置的同名cookie,默认为false,如果设置为true,所有的cookie都会被同名的替代,无论是path还是域ctx.throw([status,msg,propertioes])
抛出错误,默认是500,
ctx.throw(400);
ctx.throw(400, 'name required');
ctx.throw(400, 'name required', { user: user });
相同的效力:
const err = new Error('name required');
err.status = 400;
err.expose = true;
throw err;
这是用户级的错误,并使用err.expose标记,消息适合客户端的响应,这通常不是错误信息,因为不想泄露信息。
ctx.throw(401, 'access_denied', { user: user });
您可以选择传递一个属性对象,该对象按原样合并到错误中,对于装饰向上游请求者报告的机器友好错误很有用。
ctx.assert(value,[status],[msg],[properties])
ctx.assert(ctx.state.user, 401, 'User not found. Please login!');
ctx.response
能够设为ctx.response=false
如果要写入原始res对象而不是让Koa为您处理响应,请使用此选项。
request aliases
ctx.header
ctx.headers
ctx.method
ctx.method=
ctx.url
ctx.url=
ctx.originalUrl
ctx.origin
ctx.href
ctx.path
ctx.path=
ctx.query
ctx.query=
ctx.querystring
ctx.querystring=
ctx.host
ctx.hostname
ctx.fresh
ctx.stale
ctx.socket
ctx.protocol
ctx.secure
ctx.ip
ctx.ips
ctx.subdomains
ctx.is()
ctx.accepts()
ctx.acceptsEncodings()
ctx.acceptsCharsets()
ctx.acceptsLanguages()
ctx.get()
response aliases
ctx.body
ctx.body=
ctx.status
ctx.status=
ctx.message
ctx.message=
ctx.length=
ctx.length
ctx.type=
ctx.type
ctx.headerSent
ctx.redirect()
ctx.attachment()
ctx.set()
ctx.append()
ctx.remove()
ctx.lastModified=
ctx.etag=
Error对象也可用于用户自定义的异常的基础对象
new Error([message[,filename[,lineNumber]]])
filename:默认是调用Error构造器代码所在的文件 的名字
lineNumber:默认是调用Error构造器代码所在的文件的行号
当代码运行时的发生错误,会创建新的Error对象,并将其抛出。
EvalError
:创建一个error实例,表示错误的原因:与 eval()
有关。
InternalError
:创建一个代表Javascript引擎内部错误的异常抛出的实例。 如: “递归太多”.
RangeError
:创建一个error实例,表示错误的原因:数值变量或参数超出其有效范围.
ReferenceError
:创建一个error实例,表示错误的原因:无效引用。
SyntaxError
:创建一个error实例,表示错误的原因:eval()
在解析代码的过程中发生的语法错误。
TypeError
:创建一个error实例,表示错误的原因:变量或参数不属于有效类型。
URIError
:创建一个error实例,表示错误的原因:给 encodeURI()
或 decodeURl()
传递的参数无效。
Error.prototype.constructor
Error.prototype.message
Error.prototype.name
厂家扩展:
Error.prototype.description
类似于message
Error.prototype.number
错误码
Error.prototype.fileName
产生该错误的文件名
Error.prototype.lineNumber
产生该错误的行号
Error.prototype.columnNumber
产生该错误的列号。
Error.prototype.stack
错误堆栈
Error.prototype.toSource()
返回一个包含特定 Error
对象的源代码字符串,你可以用该值新建一个新的对象,重写自 Object.prototype.toSource()
方法。
Error.prototype.toString()
返回一个表示该对象的字符串,重写自 Object.prototype.toString()
方法
处理基本错误
try {
throw new Error("Whoops!");
} catch (e) {
alert(e.name + ": " + e.message);
}
处理特定的错误:
判断异常的类型来特定处理某一类的异常,即判断 constructor
属性,当使用现代Javascript引擎时,可使用instanceof
关键字:
try {
foo.bar();
} catch (e) {
if (e instanceof EvalError) {
alert(e.name + ": " + e.message);
} else if (e instanceof RangeError) {
alert(e.name + ": " + e.message);
}
// ... etc
}
共有7个类型的错误构造函数,上述。
自定义异常类型:
自定义基于Error的异常类型,使得你能够 throw new MyError() 并可以使用 instanceof MyError
来检查某个异常的类型. 这种需求的通用解决方法如下.
注意,在FireFox中抛出自定义类型的异常会显示不正确的行号和文件名。
// Create a new object, that prototypally inherits from the Error constructor.
function MyError(message) {
this.name = 'MyError';
this.message = message || 'Default Message'; this.stack = (new Error()).stack;
}
MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;
try {
throw new MyError();
} catch (e) {
console.log(e.name); // 'MyError'
console.log(e.message); // 'Default Message'
}
try {
throw new MyError('custom message');
} catch (e) {
console.log(e.name); // 'MyError'
console.log(e.message); // 'custom message'
}