TypeScriptのエラーハンドリング用カスタムミドルウェアの作り方完全ガイド!Node.jsとExpressによるバックエンド開発初心者向け解説
生徒
「TypeScriptを使ってウェブサーバーの裏側のプログラムを作っているのですが、プログラムが予期せぬエラーを起こしたときに、画面が真っ白になったり怪しい英語のエラー画面が出たりして困っています。スマートに解決する方法はありませんか?」
先生
「それはバックエンド開発において非常に重要な課題ですね。TypeScriptとExpressの組み合わせでは、エラーハンドリング用のカスタムミドルウェアという仕組みを作ることで、すべてのエラーを一箇所で綺麗に処理して、利用者に優しいメッセージを返すことができるようになりますよ。」
生徒
「カスタムミドルウェア、なんだか難しそうな名前ですね。パソコンにあまり触ったことがない私でも作れるようになりますか?」
先生
「大丈夫です!専門用語の言葉の意味から、実際のプログラムの書き方まで、図で考えるように分かりやすく順番に解説していきます。それでは、エラーハンドリングの基本から一緒に見ていきましょう!」
1. バックエンド開発とエラーハンドリングとは
まずは、私たちが作っているシステムの世界観からお話しします。インターネットでウェブサイトを見たり、スマートフォンのアプリを使ったりするとき、私たちの手元にある画面の裏側では、バックエンドと呼ばれるサーバー側のプログラムが動いています。このバックエンドの役割は、データベースから情報を探してきたり、計算を行ったりして、結果を画面に送り返すことです。
しかし、プログラムはいつでも完璧に動くとは限りません。存在しないページにアクセスされたり、入力されたデータが間違っていたり、通信が途切れたりといった、様々なトラブルが発生します。このトラブルのことをプログラミングの世界ではエラーと呼びます。
もしエラーが起きたときに何も対策をしていないと、システムが突然動かなくなったり、利用者の画面に変な英語の文章が表示されて不信感を与えてしまったりします。そこで、エラーが発生したときに「問題が起きました」と安全に、かつ分かりやすく利用者に伝えるための仕組みが必要になります。この仕組みのことをエラーハンドリングと呼びます。ハンドリングとは、車のハンドルを操作するように、物事をうまく制御して処理するという意味です。
2. Node.jsとExpressとTypeScriptの関係性
今回使用する道具について、パソコン初心者の方にもイメージしやすいように解説します。まず、Node.js(ノードジェーエス)とは、パソコン上でサーバー側のプログラムを動かすための土台となる仕組みです。本来はウェブブラウザの中でしか動かなかったプログラミング言語を、パソコンのシステム上で直接動かせるようにしたものです。
次に、Express(エクスプレス)とは、Node.jsの上でウェブサーバーを簡単に作るための便利な道具箱のようなものです。これを使うことで、特定のURLにアクセスがあったらこの処理をする、といった交通整理が驚くほど簡単に行えるようになります。
そして、TypeScript(タイプスクリプト)は、プログラムの書き間違いを事前に防いでくれる、非常に賢い見守り役の言語です。データに「これは文字です」「これは数字です」という目印をつけるルールがあり、開発者が間違った使い方をしようとすると、プログラムを実行する前に「ここが間違っていますよ」と赤線を出して教えてくれます。この三つの道具を組み合わせることで、安全で頑丈なバックエンドのシステムを作ることができます。
3. カスタムミドルウェアの仕組みと役割
ここで、今回の主役であるカスタムミドルウェアについて説明します。まずミドルウェアとは、Expressのシステムにおいて、リクエスト(利用者からの要求)が届いてから、レスポンス(利用者への返答)を返すまでの間に割り込んで、特定の仕事をこなすプログラムのことです。駅の改札口や、工場の検品所のようなイメージです。
通常、利用者からのリクエストは、順番にいくつかの処理を通過していきます。カスタムミドルウェアの「カスタム」とは、「自分で手作りした」という意味です。つまり、エラーが発生したときにだけ自動的に呼び出される、自分専用の検品所をプログラムの通り道に設置するのです。どこでどんなエラーが起きても、最終的にこの手作りの検品所にエラーが集まってくるように設定することで、プログラムのあちこちにバラバラのエラー対策を書く必要がなくなり、一箇所でまとめて綺麗に処理ができるようになります。
4. 最もシンプルなエラーハンドリングのコード
それでは、実際にTypeScriptを使って、Expressでの最も基本的なエラーハンドリングのプログラムを書いてみましょう。まずは一番シンプルで、仕組みが理解しやすい形からスタートします。下記のプログラムを見てください。
import express, { Request, Response, NextFunction } from 'express';
const app = express();
app.get('/error-test', (req: Request, res: Response, next: NextFunction) => {
const err = new Error('何らかの不具合が発生しました。');
next(err);
});
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
res.status(500).send('サーバーでエラーが発生しました。しばらくお待ちください。');
});
app.listen(3000);
このプログラムの解説をします。app.getという部分は、利用者から特定のページへのアクセスを待ち受ける設定です。その中で、わざとエラーを作り出して、next(err)という命令を呼び出しています。このnextという関数にエラーを渡すことで、Expressは「通常の処理を中断して、エラー処理用のプログラムにバトンを渡しなさい」と判断します。
その後にあるapp.useの部分が、エラーを受け取るミドルウェアです。通常のミドルウェアと異なり、一番最初にerrという引数を持っているのが特徴です。これにより、トラブルが発生したときに自動的にこの場所へワープしてくるようになります。
5. 実行結果の確認とステータスコードの意味
先ほどのプログラムをパソコンで実行して、インターネットブラウザなどでアクセスしたときの画面の動きについて説明します。プログラムを実行すると、パソコンの中に仮想のサーバーが立ち上がります。その状態で指定のページを開くと、画面には次のような文字が表示されます。
サーバーでエラーが発生しました。しばらくお待ちください。
このように、英語の複雑なエラー画面ではなく、自分たちが用意した日本語の親切なメッセージが表示されました。ここで重要なのが、プログラムの中にあるstatus(500)という数字です。これはHTTPステータスコードと呼ばれるもので、インターネットの世界で通信の結果を表す共通の番号です。数字の500は「サーバーの内部で何か問題が発生しました」ということを意味しています。この番号を正しく設定してあげることで、ブラウザや他のシステムに対しても、何が起きたのかを正確に伝えることができます。
6. TypeScriptの型定義を活用した安全なエラー設計
次に、TypeScriptの強みを活かした、より本格的なプログラムに進化させていきましょう。先ほどの例では、エラーの種類をanyという「何でも許容する」設定にしていましたが、これではTypeScriptの見守り機能が働きません。そこで、エラーの情報をしっかりと整理するための設計図を作ります。
class CustomError extends Error {
statusCode: number;
constructor(statusCode: number, message: string) {
super(message);
this.statusCode = statusCode;
}
}
const badRequestError = new CustomError(400, '入力されたデータが正しくありません。');
console.log(badRequestError.statusCode);
console.log(badRequestError.message);
このコードでは、元々用意されているエラーの仕組みを改造して、自分専用のCustomErrorという新しいエラーの型(クラス)を作っています。これにより、エラーのメッセージだけでなく、先ほど説明したステータスコードの数字も一緒にセットにして持ち運ぶことができるようになります。プログラミング未経験の方には難しく見えるかもしれませんが、要するに「エラーの種類と、対応する番号をひとまとめにした専用の箱」を作ったと考えてください。これで、どんなエラーなのかを明確に区別できるようになります。
7. 汎用的なカスタムミドルウェア関数の作成
それでは、いよいよ本番です。先ほど作った専用の箱を受け取って、自動的に適切な返答を組み立てる、完成版のカスタムミドルウェアを作成してみましょう。実際の開発でもよく使われる、実用的な形を記述します。
import { Request, Response, NextFunction, ErrorRequestHandler } from 'express';
class AppError extends Error {
statusCode: number;
constructor(statusCode: number, message: string) {
super(message);
this.statusCode = statusCode;
}
}
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.message || '予期せぬエラーが内部で発生しました。';
res.status(statusCode).json({
status: 'error',
statusCode: statusCode,
message: message
});
};
このプログラムでは、ErrorRequestHandlerというExpressが公式に用意してくれている型を使用しています。これにより、引数の設定がエラー用であることをTypeScriptが正しく認識してくれます。ミドルウェアの中では、受け取ったエラーにステータスコードが含まれているかを確認し、もし無ければ安全のために500番(サーバーの不具合)として扱います。そして、jsonという形式を使って、データとして綺麗に整理整頓された形で利用者にエラー情報を返却しています。
8. 作成したミドルウェアをアプリケーションへ組み込む方法
作ったエラーハンドリング用のカスタムミドルウェアは、ただ書いた件だけでは動きません。Expressで作ったサーバー全体の仕組みに対して、「これを使ってね」と登録してあげる必要があります。その登録方法を含めた全体の連動コードを確認しましょう。
import express from 'express';
const app = express();
app.get('/user', (req, res, next) => {
const userId = req.query.id;
if (!userId) {
return next(new AppError(400, 'ユーザーの識別番号が指定されていません。'));
}
res.send('ユーザーの情報を表示します。');
});
app.use(errorHandler);
app.listen(3000);
ここで非常に大切なルールがあります。それは、app.use(errorHandler)というエラーを処理するミドルウェアの登録は、必ず他のすべての処理よりも一番最後に書かなければいけないという点です。プログラムは上から下へと順番に読み込まれて実行されるため、途中の道で発生したすべてのエラーを一番下にある網でキャッチするような仕組みになっているからです。この順番を間違えると、エラーを捕まえることができずに素通りしてしまうので注意してください。
9. 本番環境と開発環境でのエラー表示の切り替え
実際のシステム開発では、もう一つの工夫を行います。それは、開発者が自分のパソコンでプログラムを直しているとき(開発環境)と、実際に世界中の利用者が使っているとき(本番環境)で、エラーの表示内容を切り替えることです。開発しているときは、どこが間違っているのか詳しく知りたいですが、一般の利用者にプログラムの中身を詳しく見せてしまうのは、セキュリティ上のリスクになりますし、見た目もよくありません。
const envHandler: ErrorRequestHandler = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
const isDevelopment = process.env.NODE_ENV === 'development';
if (isDevelopment) {
res.status(statusCode).json({
message: err.message,
stack: err.stack
});
} else {
res.status(statusCode).json({
message: 'システムエラーが発生しました。'
});
}
};
このプログラムでは、現在の環境がどちらであるかを自動で判定して、処理を分岐させています。開発環境であれば、err.stackという「プログラムの何行目でエラーが起きたか」という詳細なレポートを表示し、本番環境であれば、シンプルなメッセージだけを返すようにしています。このように、状況に応じて表示を変えるカスタムミドルウェアにしておくことで、開発効率を高めつつ、安全なバックエンドシステムを運営することができるようになります。