# 1. 前言

在之前的所有文章中,我们都默认所有处理逻辑都正常运行,不会出现异常,所以我们没有对异常进行处理。但是这显然是理想情况,真实项目中谁也不敢保证代码运行不会出现任何异常,为了防止代码跑崩,我们需要对异常进行统一处理。

不但要处理常规异常,我们也要对一些特殊情况进行兼容处理,使其能够返回可读性强的信息。例如,当请求的路由不存在时,此时会直接返回 Not Found ,而我们更希望返回 {status:404,msg:'接口不存在'} ;当请求携带的 Token 过期时,此时会直接返回 Authorization Error,而我们更希望返回 {status:401,msg:'未授权'} 等等一些情况,所以我们有必要对这些情况进行统一处理一下。

# 2. 整理思路

我们要对进来的所有请求都做异常处理,那么编写一个中间件来干这件事是最合适不过的了。既然要编写自定义中间件,那么我们回到 「02.中间件」这一章节,编写中间件之前我们问自己两个问题:

  • 我要拦截什么?是请求还是响应?

    答:是响应。因为我们要处理的是路由响应请求的过程中如果发生错误,我们对其统一处理,所以我们要拦截的响应。

  • 在什么阶段拦截?

    答:我们希望请求进来后,经过层层中间件处理,再到响应即将出去,这中间任何一个环节出现异常,我们都要拦截处理,所以这个中间件应该处于所有中间件的最外层,这样才能确保请求一进来就先经过这个中间件,经过层层处理,最终响应出去也最后经过这个中间件。

OK,明确了这两个问题之后,我们就可以开始着手编写错误处理的中间件了。

# 3. 代码实现

# 3.1 创建文件

基于代码分层后目录结构,我们在 middleware 文件夹下创建一个名为 errorHandler.ts 文件,存放错误处理中间件的逻辑代码。

middleware/
├─ jwt.ts
├─ errorHandler.ts
└─ index.js
1
2
3
4

# 3.2 搭建中间件骨架

import * as Koa from "koa";

export default async (ctx: Koa.Context, next: Koa.Next) => {
  try {
    await next();
  } catch (e) {
    /*此处进行错误处理,下面会讲解具体实现*/
  }
};
1
2
3
4
5
6
7
8
9

如上所述,我们要对响应进行拦截,所以我们需要将具体逻辑写在 next() 之后,因为我们要捕获在这个中间件之前所有环节中可能出现的错误,所以我们将 next()try catch 包裹起来,在 catch 中编写具体的错误处理逻辑。

# 3.3 兼容 404 情况

当请求一条不存在的路由时,此时应该返回友好的 404 错误。由于路由不存在这种情况,它并不是一种代码运行异常,只是服务做出的默认的动作对我们来说不友好而已,所以不会触发 try catch,为了统一在 catch 中处理方便起见,我们手动将其作为异常抛出,如下:

try {
  await next();
  /**
   * 如果没有更改过 response 的 status,则 koa 默认的 status 是 404
   */
  if (ctx.status === 404 && !ctx.body) ctx.throw(404);
} catch (e) {}
1
2
3
4
5
6
7

这样一来,我们就能在 catch 中处理了。

# 3.4 兼容 401 情况

同理,未授权情况也不会触发 try catch,我们也需要手动地将其作为异常抛出,如下:

try {
  await next();
  if (ctx.status === 401 && !ctx.body) ctx.throw(401);
} catch (e) {}
1
2
3
4

# 3.5 错误统一处理逻辑

错误处理逻辑其实很简单,就是对错误码进行判断,并返回对应的信息。这段代码运行在错误 catch 中。

try {
  await next();
  if (ctx.status === 404 && !ctx.body) ctx.throw(404);
  if (ctx.status === 401 && !ctx.body) ctx.throw(401);
} catch (e) {
  const status = parseInt(e.status);
  // 默认错误信息为 error 对象上携带的 message
  let msg = e.message;
  // 对 status 进行处理,指定错误页面文件名
  if (status >= 400) {
    switch (status) {
      case 401:
        msg = "未授权";
        break;
      case 404:
        msg = "接口不存在";
        break;
      case 500:
        msg = "服务发生异常";
        break;
    }
  }
  ctx.body = {
    status,
    msg,
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

Ok,以上我们就编写完了错误处理中间件的所有逻辑。

# 4. 引入中间件

修改 middleware/index.ts,引入 errorHandler 中间件,并将它放到洋葱模型的最外层,如下:

import * as Koa from "koa";
import * as bodyParser from "koa-bodyparser";
import errorHandler from "./errorHandler";

export default (app: Koa) => {
  app.use(errorHandler); // 应用错误处理中间件
  app.use(bodyParser());
};
1
2
3
4
5
6
7
8

# 5. 测试

  • 当我们不携带 Token 请求时,返回的结果已经是我们经过处理后的样子了,如下:

  • 当访问一个不存在的接口时:

  • 当我们在登录接口中手动抛出异常来模拟接口出错的情况时: