【express】エラーハンドリング

公開
更新
Photo by Minh Pham

前提

・TypeScriptをベースとしています。
・viewはejsを用いて行います。
・各用語の細かい解説は必要と感じたものだけ行います。
・expressはエラーハンドリングのやり方が一つだけではありません。あくまで一例としてご覧ください。

目的

・expressのミドルウェアを用いたエラーハンドリングをできるようになる
・同期処理と非同期処理のエラーハンドリングの違いについて理解する

エラーハンドリング

expressでのエラーハンドリング

expressでのエラーハンドリングはミドルウェアを用いて行います。
ミドルウェアはリクエストとレスポンスの間で処理を行う関数のことです。
ルーティングの際に生じたエラーをミドルウェアでキャッチしてエラー処理を行うという流れになります。

同期処理と非同期処理

同期処理と非同期処理でのエラーハンドリング(エラーを投げる処理)は異なります。
大きな違いとしてはnextを用いるかどうかです。

エラーハンドリング用ミドルウェア

まずはエラーをキャッチする用のミドルウェアを作成します。
独自のエラークラスを作って使用しているため、エラーの型にExpressErrorTypeというinterfaceを用いております。
理由は後述します。

app.use(
  (err: ExpressErrorType, req: Request, res: Response, next: NextFunction) => {
    const { status = 500 } = err
    if (!err.message) {
      err.message = '問題が起きました。'
    }
    res.status(status).render('pokemonds/error', { err })
  }
)


試しに存在しないページへのリクエストを発生させてみます。
new ExpressErrorについては後述します。
今はエラーを処理するためのクラスと思っていただければOKです。

// 存在しないページへのリクエスト
app.all('*', (req: Request, res: Response, next: NextFunction) => {
  next(new ExpressError('ページが見つかりませんでした。', 404))
})

app.allによってHTTPメソッドを用いたリクエストかつ存在しないURLへのリクエストをキャッチして、
ミドルウェアに処理を渡します。
nextの引数にエラーを渡すことによってエラーハンドリング用のミドルウェアに処理が渡ります。

独自のエラークラスを作る

さて、さっきからExpressErrorTypeとかExpressErrorとか出てきて戸惑われた方もいると思います。
こちらを用いる意図と作成の手順を解説します。

作る意図

では、中身を作りながら解説します。
今回汎用的なクラスを作成して使用しますので、utilsディレクトリ配下で行います。

// src/utils/ExpressError.ts

export class ExpressError extends Error {
  status: number
  constructor(message: string, status: number) {
    super()
    this.message = message
    this.status = status
  }
}


組み込みのErrorクラスを継承して作成しています。
簡単に説明すると、初期化の段階でmessageとstatusというプロパティをインスタンスに定義します。
では、Errorクラスの型定義を見てみましょう。

interface Error {
    name: string;
    message: string;
    stack?: string;
}


statusが含まれていません。statusはステータスコードのことです。
よってこちらを継承して新しくクラスを作成し、statusを定義する必要があります。

なぜstatusが必要なのか

理由は二つあります。
statusによってエラーの種類がわかる
expressの性質上err.status(または、err.statusCode)がレスポンスのステータスコードとして設定される

statusによってエラーの種類がわかる

例えばステータスコードが404の場合、存在しないページへのリクエストがあったとわかります。
500だった場合、サーバー側の処理で何かしらのエラーが起こってるのだとわかります。
このようにステータスコードという数字を見るだけでエラーの概要を把握することができます。

expressの性質

expressはミドルウェアによって受け取った、err.status(err.statusCode)をレスポンスのステータスコードとして設定するという性質があります。
これは英語版のexpress公式によって紹介されています。日本語版には載ってないみたいです。。。

・The res.statusCode is set from err.status (or err.statusCode). If this value is outside the 4xx or 5xx range, it will be set to 500.
・The res.statusMessage is set according to the status code.
Express - The default error handler


結論

expressは

res.status(404).send('エラーが発生しました!')


このようにしてレスポンスのステータスにステータスコードを設定して、エラーを返すことができます。
しかしstatus()という記述をエラー処理のたびに毎回行うと冗長ですし、忘れる可能性もあります。
するとデフォルトで500エラーが返ります。
それを避けるためにこのようなクラスを作成します。

同期処理のエラーハンドリング

例えばパスワード認証がないと入れないページがあったとします。

const checkPassword = (req: Request, res: Response) => {
    // passwordが「 ispassword 」という前提
    if(req.body.password === 'ispasword') {
        next()
    }
    throw new ExpressError('パスワードが違います。', 401)
}

app.get('/seacretpage', checkPassword, (req: Request, res: Response) => {
  res.send('ここは認証された人のみが入れるページです!')
})


要点は、
ルーティングメソッドはミドルウェアを複数引数に取れる
認証された人とそうでない人で処理を分けている

複数のミドルウェア

ルーティングの際ミドルウェア関数を複数引数に取ることができます。
そのため、認証の関数を挟んで、次の処理を実行するか決めるという処理を行うことができます。

認証された人とそうでない人で処理を分けている

passwordという値に「ispassword」という値を入れた人のみ次のミドルウェアに処理が移ります

if(req.body.password === 'ispassword') {
        next()
    }


認証されなかった人(ispassword以外を入力した人)はエラーが投げられます。
エラーがthrowされることによって後続の処理は中止されエラーハンドリング用のミドルウェアが発火します。

throw new ExpressError('パスワードが違います。', 401)


結論

同期処理はエラーをthrowすることでエラーハンドリングを行うことができます。

非同期処理のエラーハンドリング

こちらも同じくエラーをthrowしてみるとうまくいきません。
statusを見るとpending状態で止まります。
通信が終わることはありません。これはエラーハンドリング用のミドルウェアに処理が移ってないためだと考えられます。


ではどのようにして渡せば良いでしょうか。
結論から言うと、nextの引数にエラーを渡します。

next(new ExpressError('エラー', 401))


エラーをキャッチする

app.get('/pokemons', async (req: Request, res: Response, next: NextFunction) => {
  try {
    const pokemons = await Pokemons.find({})
    res.render('pokemons/index', { pokemons })
  } catch (err) {
    next(new ExpressError('エラー', 401))
  }
})


try/catch構文を使ってエラーをキャッチします。
しかしこれを毎回非同期処理のたびに書くのは冗長です。
同じことをやるのでutilsにそれ用の関数を定義します。

// catchAsync.ts
import { Request, Response, NextFunction } from 'express'

export const catchAsync = (
  fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
) => {
  return (req: Request, res: Response, next: NextFunction) => {
    fn(req, res, next).catch((e) => next(e))
  }
}


型定義が非常にあやしいですが・・・
関数を返す関数を定義して、その中でPromiseを返す関数からcatchを呼んであげます。
この関数で非同期処理が生じるミドルウェアをラップしてあげます。

app.get(
  '/pokemons',
  catchAsync(async (req: Request, res: Response) => {
    const pokemons = await Campground.find({})
    res.render('pokemons/index', { pokemons })
  })
)


非同期のasyncがつくミドルウェアはすべてこれでラップしてエラーハンドリングを行うことができます。

まとめ

・expressはerr.statusをレスポンスのステータスコードとして設定する
・同期処理はthrowすることでミドルウェアに処理を移す
・非同期処理はthrowではpending状態となるためcatchするための共通関数を作ってラップ。

輝良 / Kira

HTML, CSS, JavaScript, Vueを勉強して、未経験から独学でフロントエンドエンジニアへ転職。 実務ではTypeScriptとVueを使用。モダンフロントエンド技術が好き。 当サイトはNuxt3+TS+TailwindCSS+microCMSで構築。