如何处理Express中的错误(附代码示例)

279 阅读6分钟

在Express中处理错误往往是事后的想法。然而,它通常应该从你的Express路由开始,因为如果发生错误,你的用户需要得到通知。有了正确的技术,Express中的错误处理就变得非常简单了。

有几种情况可能会发生错误。可能是用户没有提供RESTful请求的所有信息,你的ORM/ODM层(例如Mongoose)不能在数据库上执行所需的操作(由于验证或其他一些限制),或者用户没有被授权在RESTful资源上执行某种操作。

在这一节中,我们将通过一个数据库验证案例,该案例是我们之前在设计数据库模型时创建的,用户不被允许创建一个文本为空的消息实体。首先,尝试在命令行中执行以下cURL操作。

curl -X POST -H "Content-Type:application/json" http://localhost:3000/messages -d '{"text":"Hi again, World"}'

在这种情况下,一切工作都没有任何错误,因为提供了一个text 。然而,如果你把text 空着,你会得到一个不同的结果。

curl -X POST -H "Content-Type:application/json" http://localhost:3000/messages -d '{"text":""}'

命令行甚至可能被卡住,因为REST API没有提供任何响应。可能会有一个消息,比如。"来自服务器的空回复"。在你的服务器的日志中,你可能会看到类似*"UnhandledPromiseRejectionWarning:未处理的承诺拒绝"。如果我们仔细检查src/models/message.js*文件,我们会发现不允许创建一个带有空text 属性的消息,因为它是必需的。

...

const messageSchema = new mongoose.Schema(
  {
    text: {
      type: String,
      required: true,
    },
    user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
  },
  { timestamps: true },
);

...

因为这个数据库验证,如果有一个空的text ,Mongoose会抛出一个错误,这需要在我们的Express路由中解决,我们在src/routes/message.js文件中创建消息。

router.post('/', async (req, res) => {
  let message;
  try {
    message = await req.context.models.Message.create({
      text: req.body.text,
      user: req.context.me.id,
    });
  } catch (error) {
    return res.status(400).json({ error: error.toString() });
  }

  return res.send(message);
});

再次尝试执行之前的cURL操作。现在你可能会在命令行上收到类似的输出。

{"error":"ValidationError: text: Path `text` is required."}

这是一些进步,因为我们从一个源于数据库层的未处理的错误,使我们的服务器被卡住,到在这个特定的Express路由中处理这个错误。由于我们使用的是async/await,在这种情况下使用try/catch块是一种常见的做法。在这种情况下,我们只是返回了一个400的HTTP状态代码,这通常用于验证错误,而验证错误是由客户端应用程序的错误输入引起的。

如果我们想为我们的每条Express路线进行错误处理,我们最终会有很多的try/catch块,它们大多会为我们执行所有相同的错误处理。对于一个有很多REST API端点的大型Express应用程序来说,这根本无法扩展。幸运的是,我们有Express的中间件,可以在我们的src/index.js文件中统一处理这些错误:

...

app.use('/session', routes.session);
app.use('/users', routes.user);
app.use('/messages', routes.message);

app.use((error, req, res, next) => {
  return res.status(500).json({ error: error.toString() });
});

...

Express将任何有四个参数的路由处理为错误处理中间件,所以在这里列出错误处理回调函数的所有四个参数很重要。把这个中间件列在你的REST API路由之后也很重要,因为只有这样,你的REST API端点中发生的所有错误才能被委托给这个错误处理中间件。

让我们回到src/routes/message.js中创建一个消息。如果你尝试cURL请求,如果你没有捕捉到任何错误,你将不会看到错误处理的发生。在Express中,错误必须通过next 函数明确地发送给中间件。幸运的是,我们不需要使用try/catch块,而只是使用promise的catch方法。

router.post('/', async (req, res, next) => {
  const message = await req.context.models.Message.create({
    text: req.body.text,
    user: req.context.me.id,
  }).catch(next);

  return res.send(message);
});

现在,当你再次尝试创建一个没有text 的消息时,你应该看到返回的错误。这已经是Express中错误处理的要点了,然而,我还想向你展示一些东西。

首先,我们在这里总是返回一个通用的 HTTP 状态代码 500。这对于大多数来自数据库的验证错误来说可能是正常的,但是,它不应该成为所有错误的默认情况。让我们用另一个状态代码来体验这个场景。因此,我们将在src/index.js文件中创建一个新的 Express 路由。

app.use('/session', routes.session);
app.use('/users', routes.user);
app.use('/messages', routes.message);

app.get('/some-new-route', function (req, res, next) {
  res.status(301).redirect('/not-found');
});

app.use((error, req, res, next) => {
  return res.status(500).json({ error: error.toString() });
});

在浏览器中访问这个路由将导致重定向到404 not found页面(我们还没有实现)。301 HTTP状态代码总是表示重定向,Express的redirect 方法让我们以编程方式执行这种重定向。

现在,我们想对所有不被我们的API匹配的路由进行通用重定向。因此,我们可以使用一个带有* 星号的通配符路由,但是我们还需要确保将这个路由作为我们所有路由的最后一个路由。

app.use('/session', routes.session);
app.use('/users', routes.user);
app.use('/messages', routes.message);

app.get('*', function (req, res, next) {
  res.status(301).redirect('/not-found');
});

最后,我们可以让我们的中间件来处理这种情况,让通配符路由直接抛出一个错误:

app.get('*', function (req, res, next) {
  const error = new Error(
    `${req.ip} tried to access ${req.originalUrl}`,
  );

  error.statusCode = 301;

  next(error);
});

app.use((error, req, res, next) => {
  if (!error.statusCode) error.statusCode = 500;

  if (error.statusCode === 301) {
    return res.status(301).redirect('/not-found');
  }

  return res
    .status(error.statusCode)
    .json({ error: error.toString() });
});

如果在错误对象处没有statusCode 属性(这是默认的),我们将把HTTP头的状态码设置为500。如果有一个可用的状态码,我们将使用这个状态码进行响应。在有301状态代码的特殊情况下,我们将进行重定向响应。

在我们创建消息的情况下,我们可能想为错误指定一个400的HTTP状态代码,就像我们之前做的那样,因此要调整src/routes/message.js文件中的代码:

router.post('/', async (req, res, next) => {
  const message = await req.context.models.Message.create({
    text: req.body.text,
    user: req.context.me.id,
  }).catch((error) => {
    error.statusCode = 400;
    next(error);
  });

  return res.send(message);
});

你也可以选择将其提取为一个可重用的自定义错误,它可以位于src/utils/errors.js文件中。这个新的错误类扩展了常用的JavaScriptError 类。我们在这里添加的唯一功能是400 HTTP状态代码。

export class BadRequestError extends Error {
  constructor(error) {
    super(error.message);

    this.data = { error };
    this.statusCode = 400;
  }
}

然后再在src/routes/message.js文件中,我们可以导入这个新的错误类,用它来处理错误。这样,我们就可以对错误做出反应,并给它附加一个合适的状态代码:

...

import { BadRequestError } from '../utils/errors';

...

router.post('/', async (req, res, next) => {
  const message = await req.context.models.Message.create({
    text: req.body.text,
    user: req.context.me.id,
  }).catch((error) => next(new BadRequestError(error)));

  return res.send(message);
});

如果我们必须处理不同的情况,所以不仅仅是验证错误,还有其他的错误,我们可以再次将错误实现为具有适当HTTP状态码的类,然后根据来自数据库的错误,有条件地决定我们要使用哪个错误。毕竟,我们总是能够控制哪些错误被传递给我们的错误处理中间件,而中间件则决定返回给我们的用户什么。