Koa 错误捕获和处理

服务端的错误/异常类型:

  • 操作错误:非程序 bug 导致的运行时错误。如:数据库连接失败、请求接口超时、系统内存用光等等。

  • 程序错误:程序 bug 导致的错误,只要修改代码就可以避免。如:尝试读取未定义对象的属性、语法错误等等。

很显然,我们真正需要处理的是操作错误,而程序错误在开发阶段就应该马上修复。

那怎么处理操作错误呢?总结一下大概有如下这些方法:

  • 直接处理。举个例子:尝试向一个文件中写入东西,但是这个文件不存在,那处理这个错误的方法就是先创建好要写入的文件。如果我们知道怎么处理错误,那直接处理就是。

  • 重试。有时某些错误可能是偶发的(比如服务器连接不稳定等),我们可以尝试对当前操作进行重试。但是一定要设置重试的超时时间、次数,避免长时间的等待卡死应用。

  • 直接将错误抛给调用方。如果不知道具体怎么处理错误,最简单的就是将错误往上抛。比如:检查到用户没有访问某个资源的权限,那我们直接throw Error(403, '没有权限')(带上 status 状态码比较好),上层代码就可以 catch 这个错误。要么展示一个统一的无权限页面给用户,或者返回一个错误 json 给调用方。

  • 写日志然后将错误抛出。这种情况一般是发生了比较致命的错误,没法处理,也不能重试,那就需要记下错误日志(方便以后定位问题),然后将错误往上抛(交给上层代码去进行统一的错误展示)。

使用中间件统一处理错误

我们可以使用强大的中间件,来实现在 koa 里优雅地对错误进行统一处理。

我们先定义一个catchError中间件:

// src/middlewares/catchError.js

const catchError = async (ctx, next) => {
  try {
    await next()
    if (ctx.status === 404) { // 只是一个抛出自定义特定错误的示例
      throw new errs.NotFound()
    }
  } catch(err) {
    if (err.errorCode) { 
      // 如果是自己主动抛出的 HttpException类 错误
      ctx.status = err.status || 500
      ctx.body = {
        code: err.code,
        message: err.message,
        errorCode: err.errorCode,
        request: `${ctx.method} ${ctx.path}`,
      }
    } else {
      // 触发 koa app.on('error') 错误监听事件,可以打印出详细的错误堆栈 log
      ctx.app.emit('error', err, ctx)
    }
  }
});

对 Error 分类

HttpException类是继承自Error的一个构造器。我们可以将错误进行细分,定义一些特定类型的异常类,从而更精细地对异常进行处理。代码如下:

// src/constant/http-exception.js

class HttpException extends Error {
  // message为异常信息,errorCode为错误码(开发人员内部约定),code为HTTP状态码
  constructor(message = '服务器异常', errorCode = 10000, code = 400) {
    super()
    this.errorCode = errorCode || 10000
    this.code = code || 400
    this.message = message || '服务器异常'
  }
}

class ParameterException extends HttpException {
  constructor(message, errorCode) {
    super()
    this.errorCode = errorCode || 10000
    this.code = 400
    this.message = message || '参数错误'
  }
}

class NotFound extends HttpException {
  constructor(message, errorCode) {
    super()
    this.errorCode = errorCode || 10001
    this.code = 404
    this.message = message || '资源未找到'
  }
}

class AuthFailed extends HttpException {
  constructor(message, errorCode) {
    super()
    this.errorCode = errorCode || 10002
    this.message = message || '授权失败'
    this.code = 401
  }
}

class Forbidden extends HttpException {
  constructor(message, errorCode) {
    super()
    this.errorCode = errorCode || 10003
    this.message = message || '禁止访问'
    this.code = 403
  }
}

module.exports = {
  HttpException,
  ParameterException,
  NotFound,
  AuthFailed,
  Forbidden,
}

error错误侦听器

上面catchError中间件中,我们让HttpException类之外的错误都手动触发koaerror侦听器,写一下事件处理函数:

// src/utils/errorHandler.js
const path = require('path');
const fs = require('fs');
const escapeHtml = require('escape-html');

const isDev = env === 'development';
const templatePath = isDev
  ? path.join(__dirname, 'templates/dev_error.html')
  : path.join(__dirname, 'templates/prod_error.html');
const defaultTemplate = fs.readFileSync(templatePath, 'utf8');

export default function errorHandler(err, ctx) {
  console.log('onerror', err)
  // 未知异常状态,默认使用 500
  ctx.status = err.status || 500

  // 获取客户端请求接受类型

  // ctx.accepts 是 request.accepts 的别名,即客户端可接受的内容类型。
  // 和其他协商 API 一样, 如果没有提供类型(没有传参数),则返回 所有 客户端可接受的类型。[ '*/*' ]
  // 如果提供了,就返回最佳匹配,即第一个匹配上的。
  // console.log(ctx.accepts())
  switch (ctx.accepts('json', 'html', 'text')) {
    case 'json':
      // ctx.type 是 response.type 的别名, 用于设置响应头 Content-Type
      ctx.type = 'application/json'
      ctx.body = { code: ctx.status, message: err.message }
      break
    case 'html':
      ctx.type = 'text/html'
      ctx.body = defaultTemplate
        .replace('{{status}}', escapeHtml(err.status))
        .replace('{{stack}}', escapeHtml(err.stack));
      break
    case 'text':
      ctx.type = 'text/plain'
      ctx.body = err.message
      break
    default:
      ctx.throw(406, 'json, html, or text only')
  }
}

然后在app.js中将catchError中间件置于最上方(所有其他中间件之前,鉴于 koa 的洋葱圈原型);
把包含HttpException类的对象作为常量挂在全局,并用app.on('error', errorHandler)监听非自定义错误。

const path = require('path')
const Koa = require('koa')
const serve = require('koa-static')
const logger = require('koa-logger')
const koaBody = require('koa-body')
const config = require('config')
const mongoose = require('mongoose')
const catchError = require('@/middlewares/catchError')

import errorHandler from '@/utils/errHandler.js'
import adminRouter from '@/routes/admin'
import indexRouter from '@/routes/index'

// 全局定义一些异常类型,方便针对性抛出
const errors = require('@/constant/http-exception')
global.errs = errors

const app = new Koa()

/* koa 是洋葱模型
  一个请求匹配到的所有中间件,从【第一个】开始执行,一旦执行到 await next() 的时候就会暂停,进入到下一个匹配的中间件;
  到【最后一个】再回过头来倒着处理每个中间件 await 后面的程序,直到执行到【第一个】中间件。
*/
// 错误捕获中间件 await next() 后的内容会在最后执行,所以我们要把它放在最前面,这一点和 express 非常不同
app.use(catchError)

mongoose
  .connect(
    `mongodb://${config.get('db.user')}:${config.get('db.pwd')}@${config.get(
      'db.host'
    )}:${config.get('db.port')}/${config.get('db.name')}`
  )
  .catch(err => {
    console.log(err)
    throw new errs.HttpException('数据库连接失败')
  })

// 静态资源服务中间件
app.use(serve(path.join(process.cwd(), 'public')))

// 记录日志中间件
app.use(logger())

// 处理 post 请求参数的中间件
app.use(koaBody())

// 注册管理后台路由中间件
app.use(adminRouter.routes()).use(adminRouter.allowedMethods())
// 注册前台路由中间件
app.use(indexRouter.routes())

// 错误监听器
app.on('error', errorHandler)

app.listen(3003, err => {
  if (err) throw err
  console.log('runing at 3003')
})

比如我们在路由处理程序中要抛出这些特定类型异常(我们定义好的HttpException类):

const Router = require('koa-router')
const router = new Router()

router.post('/user', (ctx, next) => {
  if(true){
    throw new errs.HttpException('网络请求错误', 10001, 400)
  }
})

catchError中间件中写的'throw new errs.NotFound()'同理。

完整的代码 GitHub 地址是 https://github.com/aizawasayo/animal_server.git,仍在更新维护中,仅供参考。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容

  • 中间件的异常 全局异常中间件全局异常监听定义异常的返回结果定义常见的异常状态开发环境 异常查看 对于异常,我们可以...
    席坤阅读 432评论 0 0
  • 1.500错误koa提供了ctx.throw()方法来抛出错误,ctx.throw(500)就是抛出500错误 2...
    冷小谦阅读 5,244评论 0 0
  • koa错误处理 使用koa的时候,对错误的处理是比较方便直接的,我们可以写一个以下的中间件来处理错误: modul...
    awayisblue阅读 607评论 0 0
  • Koa异常处理说明 作者:zjruan 日期:2017/07/10 使用篇: Demo: 一、Controller...
    畵毣阅读 8,539评论 0 5
  • 陆陆续续用了koa和co也算差不多用了大半年了,大部分的场景都是在服务端使用koa来作为restful服务器用,使...
    Sunil阅读 1,526评论 0 3