Black Lives Matter(黒人の命は大切だ)。
Equal Justice Initiativeを支援しましょう.

エラーハンドリング

エラーハンドリングとは、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ルーターによって提供されるデフォルトのエラーオブジェクトで呼び出されます。

(文字列'route'を除く)何かをnext()関数に渡すと、Expressは現在のリクエストをエラーとして扱い、残りの非エラー処理ルーティングおよびミドルウェア関数をスキップします。

シーケンス内のコールバックがデータを提供せず、エラーのみを提供する場合、このコードを次のように単純化できます。

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

上記の例では、nextfs.writeFileのコールバックとして提供されており、エラーの有無にかかわらず呼び出されます。エラーがない場合は2番目のハンドラーが実行され、それ以外の場合はExpressがエラーをキャッチして処理します。

ルートハンドラーまたはミドルウェアによって呼び出される非同期コードで発生するエラーをキャッチし、処理のためにExpressに渡す必要があります。例えば

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

上記の例では、try...catchブロックを使用して非同期コードでエラーをキャッチし、Expressに渡しています。try...catchブロックが省略された場合、Expressは同期ハンドラーコードの一部ではないため、エラーをキャッチしません。

try...catchブロックのオーバーヘッドを回避するため、またはPromiseを返す関数を使用する場合は、Promiseを使用します。例えば

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

Promiseは同期エラーと拒否されたPromiseの両方を自動的にキャッチするため、最後のキャッチハンドラーとしてnextを提供するだけで済みます。キャッチハンドラーには最初のエラーが引数として渡されるため、Expressがエラーをキャッチします。

非同期コードを些細なものに減らすことで、同期エラーキャッチに依存するために、ハンドラーのチェーンを使用することもできます。例えば

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_ENVproductionに設定します。

エラーが書き込まれると、次の情報が応答に追加されます。

応答の書き込みを開始した後にエラーとともに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ミドルウェアにあります。

エラーハンドラーの作成

エラー処理関数は、他のミドルウェア関数と同じ方法で定義しますが、エラー処理関数には3つではなく4つの引数(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を使用して行われたリクエストとそうでないリクエストのエラーハンドラーを定義するには、次のようにします。

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ハンドラーはスキップされますが、/a_route_behind_paywallappに残りのハンドラーは引き続き実行されます。

next()next(err)の呼び出しは、現在のハンドラーが完了したこととその状態を示します。next(err)は、上記のようにエラーを処理するように設定されているハンドラーを除いて、チェーン内の残りのすべてのハンドラーをスキップします。