express 与 koa 的异常处理

我们在代码中异常处理时,经常会使用 try/catch 来进行异常捕捉,但是如果我们希望对每个操作单独做异常处理,我们就需要写多个 try/catch 来捕捉,这样导致代码非常质量非常差,对于 async 函数,我们通常可以使用:

await a().catch(e=>{
    console.log(e);
    //其它异常处理操作;
})

在 express 中想要对每个操作进行异常处理:

app.use('/',async(req,res,next)=>{
    await a().catch(e=>{
        res.status(500).send(e);
        //异常处理 通常调用 next(e),对错误进行统一处理
        throw e;
    })
});
app.use(function(err,req,res,next){
    //错误的统一处理;
})

其实最不理想的就是 throw e ,他会导致终端显示的内容很糟糕。 koa 对异常处理做了改进,可以说是比较理想了。

await a().catch(e=>{
    ctx.throw(500,e);
    //异常处理,统一的异常处理交给 app.on('error')
})

-w924

使用 ctx.throw 来抛出用户级别的错误( err.expose = true ),而非系统级别错误,终端也不会显示相应错误。 同样 kao 使用:

app.on('error',(err, ctx) => {
    console.log('server error');
    //err 统一处理
})

可以对错误进行统一处理。 在分析 Koa 错误处理流程时,要先弄明白 Koa 运行的基本流程。 把核心放在 appliction.js 中这段代码:

callback() {
    const fn = compose(this.middleware);
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

封装了 context 对象,处理了 middleware 中间件。 处理中间件:

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }
  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

代码简短精炼,很漂亮。application 将所有 use 的 middleware 整合到数组传到 compose,返回一个函数,在 appliction 被请求时调用:

return fnMiddleware(ctx).then(handleResponse).catch(onerror);

在 fnMiddleware 函数中可以看到,他返回一个 Promise 的结果,中间件需要通过执行 next() 函数才能使下一个中间件得以执行:

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

next 即 dispatch.bind(null, i + 1),及 middleware 数组的下一个中间件。 好,基本流程搞清后,就可以很容易的反推分析错误处理流程了。 当我们执行上面的操作:

await a().catch(e=>{
    ctx.throw(500,e);
    //异常处理,统一的异常处理交给 app.on('error')
})

查看 context.js 中 throw 函数:

throw(...args) {
    throw createError(...args);
  },

createError 为 http-error 库,他为 Koa 提供了错误标示,比如我们上面提到的 err.expose。 当我们 ctx 抛出异常,及在 middleware 中抛出相关异常,则上述 fnMiddleware 执行会通过:

 try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }

Promist.reject 抛出异常在 fnMiddleware 的异常中做处理,通过上面可以看到,fnMiddleware 的异常在 err => ctx.onerror(err) 中处理,在 context.js 中 onerror :

onerror(err) {
    if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err));
    let headerSent = false;
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true;
    }
    // delegate
    this.app.emit('error', err, this);
    ......

同时返回返回错误给 application :

onerror(err) {
    if (!(err instanceof Error)) 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();
    console.error(msg.replace(/^/gm, '  '));
    console.error();
  }

application 通过 err 标示来判断对错误的终端输出与否。