错误处理

错误处理 指的是 Express 如何捕获和处理同步及异步发生的错误。Express 提供了一个默认的错误处理程序,因此你无需编写自己的程序即可开始使用。

捕获错误

确保 Express 捕获所有在运行路由处理程序和中间件时发生的错误非常重要。

路由处理程序和中间件内部同步代码中发生的错误无需额外处理。如果同步代码抛出错误,Express 将捕获并处理它。例如:

app.get('/', (req, res) => {
  throw new Error('BROKEN') // Express will catch this on its own.
})

对于路由处理程序和中间件调用的异步函数返回的错误,你必须将它们传递给 next() 函数,Express 将在此处捕获并处理它们。例如:

app.get('/', (req, res, next) => {
  fs.readFile('/file-does-not-exist', (err, data) => {
    if (err) {
      next(err) // Pass errors to Express.
    } else {
      res.send(data)
    }
  })
})

从 Express 5 开始,返回 Promise 的路由处理程序和中间件在拒绝或抛出错误时将自动调用 next(value)。例如:

app.get('/user/:id', async (req, res, next) => {
  const user = await getUserById(req.params.id)
  res.send(user)
})

如果 getUserById 抛出错误或拒绝,next 将被调用,参数为抛出的错误或拒绝的值。如果没有提供拒绝的值,next 将以 Express 路由器提供的默认 Error 对象调用。

如果你向 next() 函数传递任何内容(字符串 'route' 除外),Express 会将当前请求视为错误,并跳过任何剩余的非错误处理路由和中间件函数。

如果序列中的回调只提供错误而没有数据,你可以简化代码如下:

app.get('/', [
  function (req, res, next) {
    fs.writeFile('/inaccessible-path', 'data', next)
  },
  function (req, res) {
    res.send('OK')
  }
])

在上面的例子中,next 作为 fs.writeFile 的回调提供,无论是否有错误都会被调用。如果没有错误,第二个处理程序将被执行;否则,Express 会捕获并处理错误。

你必须捕获路由处理程序或中间件调用的异步代码中发生的错误,并将其传递给 Express 进行处理。例如:

app.get('/', (req, res, next) => {
  setTimeout(() => {
    try {
      throw new Error('BROKEN')
    } catch (err) {
      next(err)
    }
  }, 100)
})

上面的例子使用了一个 try...catch 块来捕获异步代码中的错误并将其传递给 Express。如果省略了 try...catch 块,Express 将不会捕获该错误,因为它不是同步处理程序代码的一部分。

使用 Promise 可以避免 try...catch 块的开销,或者在使用返回 Promise 的函数时。例如:

app.get('/', (req, res, next) => {
  Promise.resolve().then(() => {
    throw new Error('BROKEN')
  }).catch(next) // Errors will be passed to Express.
})

由于 Promise 会自动捕获同步错误和被拒绝的 Promise,因此你可以简单地将 next 作为最终的 catch 处理程序,Express 将捕获错误,因为 catch 处理程序会将错误作为第一个参数。

你也可以通过将异步代码简化为琐碎的形式,使用处理程序链来依赖同步错误捕获。例如:

app.get('/', [
  function (req, res, next) {
    fs.readFile('/maybe-valid-file', 'utf-8', (err, data) => {
      res.locals.data = data
      next(err)
    })
  },
  function (req, res) {
    res.locals.data = res.locals.data.split(',')[1]
    res.send(res.locals.data)
  }
])

上面的例子中,readFile 调用中有几个琐碎的语句。如果 readFile 导致错误,它会将错误传递给 Express,否则你会迅速回到链中下一个处理程序的同步错误处理世界。然后,上面的例子尝试处理数据。如果失败,同步错误处理程序将捕获它。如果你在 readFile 回调中进行了此处理,那么应用程序可能会退出,并且 Express 错误处理程序将不会运行。

无论你使用哪种方法,如果你希望 Express 错误处理程序被调用并且应用程序能够继续运行,你必须确保 Express 接收到错误。

默认错误处理程序

Express 内置了一个错误处理程序,可以处理应用程序中可能遇到的任何错误。这个默认的错误处理中间件函数被添加在中间件函数堆栈的末尾。

如果你将错误传递给 next() 并且没有在自定义错误处理程序中处理它,它将由内置的错误处理程序处理;错误将连同堆栈跟踪一起写入客户端。堆栈跟踪在生产环境中不会被包含。

将环境变量 NODE_ENV 设置为 production,以生产模式运行应用程序。

写入错误时,以下信息会添加到响应中:

如果你在开始写入响应后(例如,在向客户端流式传输响应时遇到错误)使用错误调用 next(),Express 默认错误处理程序将关闭连接并使请求失败。

因此,当你添加自定义错误处理程序时,当头信息已经发送到客户端时,你必须委托给 Express 默认错误处理程序:

function errorHandler (err, req, res, next) {
  if (res.headersSent) {
    return next(err)
  }
  res.status(500)
  res.render('error', { error: err })
}

请注意,即使存在自定义错误处理中间件,如果你在代码中多次使用错误调用 next(),默认错误处理程序也可能被触发。

其他错误处理中间件可在 Express 中间件中找到。

编写错误处理程序

定义错误处理中间件函数的方式与其他中间件函数相同,只是错误处理函数有四个参数而不是三个:(err, req, res, next)。例如:

app.use((err, req, res, next) => {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})

你应在其他 app.use() 和路由调用之后,最后定义错误处理中间件;例如:

const bodyParser = require('body-parser')
const methodOverride = require('method-override')

app.use(bodyParser.urlencoded({
  extended: true
}))
app.use(bodyParser.json())
app.use(methodOverride())
app.use((err, req, res, next) => {
  // logic
})

中间件函数内的响应可以是任何格式,例如 HTML 错误页面、简单的消息或 JSON 字符串。

出于组织(和更高级别的框架)目的,你可以定义多个错误处理中间件函数,就像处理常规中间件函数一样。例如,为使用 XHR 发出的请求和不使用 XHR 的请求定义错误处理程序:

const bodyParser = require('body-parser')
const methodOverride = require('method-override')

app.use(bodyParser.urlencoded({
  extended: true
}))
app.use(bodyParser.json())
app.use(methodOverride())
app.use(logErrors)
app.use(clientErrorHandler)
app.use(errorHandler)

在此示例中,通用的 logErrors 可能会将请求和错误信息写入 stderr,例如:

function logErrors (err, req, res, next) {
  console.error(err.stack)
  next(err)
}

在此示例中,clientErrorHandler 定义如下;在这种情况下,错误会显式地传递给下一个处理程序。

请注意,当错误处理函数中调用“next”时,你需要负责写入(并结束)响应。否则,这些请求将“挂起”,并且不符合垃圾回收的条件。

function clientErrorHandler (err, req, res, next) {
  if (req.xhr) {
    res.status(500).send({ error: 'Something failed!' })
  } else {
    next(err)
  }
}

实现“全捕获”的 errorHandler 函数如下(例如):

function errorHandler (err, req, res, next) {
  res.status(500)
  res.render('error', { error: err })
}

如果你有一个带有多个回调函数的路由处理程序,你可以使用 route 参数跳到下一个路由处理程序。例如:

app.get('/a_route_behind_paywall',
  (req, res, next) => {
    if (!req.user.hasPaid) {
      // continue handling this request
      next('route')
    } else {
      next()
    }
  }, (req, res, next) => {
    PaidContent.find((err, doc) => {
      if (err) return next(err)
      res.json(doc)
    })
  })

在此示例中,getPaidContent 处理程序将被跳过,但 app/a_route_behind_paywall 的任何剩余处理程序将继续执行。

调用 next()next(err) 表示当前处理程序已完成并处于何种状态。next(err) 将跳过链中所有剩余的处理程序,除了那些按照上述方式设置为处理错误的处理程序。

编辑此页面