Lesson 6

エラーハンドリング

Lesson 6 Chapter 1
try-catch実装

Lesson3-7では、fsモジュールを使ってファイルを読み込む方法を学習しました。実際のアプリケーションでは、読み込もうとしたファイルが存在せず、処理が止まってしまうことがあります。

本章では、このようなプログラム実行中のエラーを処理する「エラーハンドリング」について学習します。

何もプログラムを終了させることだけがエラー処理ではありません。適切なエラーハンドリングを行えば、異常発生時に処理を継続させるような仕組みを実現できます。

エラーハンドリングとは

そもそもエラーハンドリングとは、プログラム実行中に何らかの問題が発生した際、それをエラーとして処理する仕組みを指します。日本語では「例外処理」と言い換えられることが一般的です。

エラーは様々な原因により発生します。たとえば冒頭で触れたファイル読み込み処理の場合、以下ようなエラーが想定されるでしょう。

  • 指定されたファイルが存在しない
  • ファイルの読み取り権限がない
  • ファイルの形式が異なっている

他にもファイルサイズが大きく、読み込みに一定の時間がかかっている状態をエラーとみなすことも可能です。エラーハンドリングでは上記を含むすべての例外を予測し、処理を記述しておかなければなりません。

try-catch構文

幸いなことに、Node.jsにはエラーハンドリング用の構文が初めから用意されています。中でも「try-catch」は最も汎用的な構文です。

まずは以下のサンプルコードを見てみましょう。

const fs = require("fs");
                        
try {
// 存在しないファイルを読み込もうとしてエラーが発生する
  const file = fs.readFileSync("dose-not-exist-file.txt");

// エラーが発生したため、これ移行の処理は実行されません
} catch (error) {

// エラーが発生したとき実行される処理
  console.log("ファイルの読み込みに失敗しました");

} finally {

// エラーが発生したかどうかに関わらず最後に必ず実行される処理
  console.log("finally節内の処理は最後に必ず実行されます");
}

一見して分かる通り、trycatchfinallyという3つのブロックに分かれています。try-caych構文では、まずtry節内にエラーが発生しうる処理を記述し、catch節内にエラーが発生した際の処理を記述する決まりです。

もしもtry節内でエラーが発生すると、エラーが発生した処理より後の処理は実行されず、catch節に処理が移行します。

finally節は省略も可能ですが、記述するとtry節内でエラーが発生したかどうかに関わらず、ブロック内の処理が最後に必ず実行されます。

3通りのパターン

try-catach構文では、ひとつのtry節に対して、catch節とfinally節のどちらか1つが存在していなければなりません。そのためtry...catchtry...finallytry...catch...finallyの3通りの書き方が存在します。

try...finallyでは、try節内で発生したエラーはキャッチされず、finally節内の処理を実行した後にエラーが発生します。

実現したい処理に合わせて、適切な書き方を選んでください。

次のチャプターでは、throw文を使ってエラーを発生させる方法について説明します。

Lesson 6 Chapter 2
エラーを発生させる

前回のチャプターでは、ファイルの読み込み(fs.readFileSync関数)が失敗した際の処理を記述しました。このように、JavaScriptには例外の発生をプログラムに知らせるための仕組みが採用されています。

今回は、エラーの発生を示すthrow文について学習しましょう。

throw文とは

throw文を用いると、例外の発生をユーザー自身で定義できます。

まずは次のサンプルコードを確認してみましょう。

function add (x, y) {
  if (isNaN(x) || isNaN(y)) {
    throw new Error("数値ではありません");
  }
  // throw文によりエラーが発生した場合、これ以降の処理は実行されません
  return x + y;
}

上記の例では、引数のどちらかが非数であった場合の処理にthrowが使われています。throwが呼び出されると以降の処理は実行されません。関数addの呼び出し元に例外が返され、 catch節があればそこからプログラムが再開されます。

エラーオブジェクト

throwの後に置かれているnew Errorに注目しましょう。throw文では例外の値を自由に設定できますが、通常はこのように「Errorオブジェクト」が指定されます。

エラーオブジェクトはプログラムの例外を管理するためのオブジェクトで、namemessagestackなどのプロパティを持ちます。それぞれのプロパティは、前述したcatch節内でアクセス可能です。

また、new Error("エラーメッセージ")のように第1引数に文字列を渡すと、エラーオブジェクトのmesssageプロパティとして参照できます。

function add (x, y) {
  if (isNaN(x) || isNaN(y)) {
    throw new Error("数値ではありません");
  }
  // throw文によりエラーが発生した場合、これ以降の処理は実行されません
  console.log(x + y);
} catch (error) {
  // catch節内で、throwされたエラーオブジェクトにアクセスできます
  console.log(error.message); // "数値ではありません"
}

エラーを知らせることの重要性

なぜエラーを発生させる方法について学ぶのか疑問に感じるかもしれません。
一例として、数値を受け取って計算結果を出力する「電卓プログラム」の作成を考えてみましょう。きっと数字以外を受け取ったときは計算できないようにしたいと思うはずです。
さて、どのような例外処理を行うべきでしょうか?
もちろんプログラムを即時終了することも可能です。しかし、それではユーザーが入力間違いに気づかないかもしれません。適切なエラーを発生させ、「数値を入力してください」といった出力を行うことで、優れた電卓プログラムとなるはずです。

throw文の実践的な活用例

ここで、throwcatchを使ったエラーの活用例をひとつご紹介します。

半角数字ファイル名を受け取り、ファイル内容を取得するプログラムを考えてみましょう。ここまで学習してきた内容を踏まえると、次のように記述できます。

get-file-content.js
const fs = require("fs");

function getFileContent(fileName) {
  if (/^[0-9]+$/.test(fileName) === false) {
    throw new Error("ファイル名に半角数字以外が含まれています");
  } try {
    fs.readFileSync(fileName);
    console.log(data);
  } catch {
    throw new Error("ファイルの読み込みに失敗しました");
  }
}

ファイル名のチェックに「正規表現」が使われています。正規表現とは、文字列のパターンを表現するための特殊な記法です。上記の/^[0-9]+$/.test(fileName)の場合、引数fileNameが半角数字のみであればtrueを返します。

エラーの活用例として、独自のErrorサブクラスFileNameErrorを定義しましょう。これにより(ファイルの読み込みに失敗したのではなく)ファイル名が半角数字でないためにエラーが発生したことが明示されます。

get-file-content.js
const fs = require("fs");

// 独自のErrorサブクラス FileNameError を定義
class FileNameError extends Error {
  constructor(message) {
    super(message);
    this.name = "FileNameError";
  }
}

function getFileContent(fileName) {
  if (/^[0-9]+$/.test(fileName) === false) {
    // 独自に定義した FileNameError クラスを利用
    throw new FileNameError("ファイル名に半角数字以外が含まれています");
  }
  
  try {  
    fs.readFileSync(fileName);
    console.log(data);
  } catch {
    throw new Error("ファイルの読み込みに失敗しました");
  }
}

try {
  getFileContent("sample.txt");
} catch (error) {
  if (error instanceof FileNameError) {
    // 独自に定義した FileNameError クラスのインスタンスの場合
    console.log(error.message); // "ファイル名に半角数字以外が含まれています"  
  }
}

独自のErrorサブクラスを定義したことで、エラーの発生原因を特定できるようになりました。

今回の学習内容をさらに応用すれば、「機能Aの処理Bで発生した〇〇エラー」といった細分化も可能になります。また、instanceofでオブジェクトの型を判定することで、発生したエラーごとに後続の処理を分岐させることも可能です。

Lesson 6 Chapter 3
Promiseによるエラーハンドリング

ここまでのチャプターでは、Node.jsのtry/catch/finally、およびthrowを用いた例外処理について学びました。もうひとつ、Webアプリケーションの安全な実装のためには、非同期処理に関する理解が欠かせません。

そこで今回のチャプターでは、Promiseを用いた非同期処理のエラーハンドリングについて学びます。

Promiseとは

Promiseは、JavaScriptにおいて非同期処理の結果を取得するための仕組みです。具体的にはPromiseオブジェクトとして定義されており、次のような形で使用されます。

promise.js
new Promise(function(resolve, reject) {
    resolve('成功');
});

new Promise(function(resolve, reject) {
    reject('失敗');
});

このように、Promiseオブジェクトのコールバック関数(引数として渡される関数)はresolve、rejectという2つの関数を持っています。そしてコールバック関数内の処理が成功した場合はresolve、失敗した場合はrejectの関数が呼ばれる仕組みです。

引数(コールバック関数) 説明
resolve 処理が成功したときに呼び出される関数
reject 処理が失敗したときに呼び出される関数

Promise

後述するように、実用的にはPromiseは関数の戻り値として使用されます。ここではPromiseがインスタンスを生成し、resolve, rejectという2つの引数を取ることを押さえておきましょう。

一見すると、Chapter1で学んだtry/catchに似ています。ただし、Promiseはあくまでも「非同期処理の結果」を記述するための仕組みです。

Promiseを使いこなすためには、JavaScriptにおける非同期処理の仕組みを知る必要があります。順を追って学習しましょう。

非同期処理の仕組み

はじめに、次のプログラムを実行して非同期処理のイメージをつかみましょう。

sample-async.js
function sampleAsync() {
  console.log("A");
  setTimeout(() => {
  console.log("B");
  }, 1000);
  console.log("C");
}
// 実行すると次のような出力順になります
// A
// C
// B

setTimeoutは、指定した時間後にコールバック関数を実行するメソッドです。上記の例では、1,000ミリ秒(1秒)後にconsole.log("B")の処理が実行されます。

上のプログラムを実行すると、最初にAが出力され、次にCが出力され、最後にBが出力されます。

もしかすると、Aが表示されてから1秒後にBが出力されると思ったかもしれません。しかし、実際にはsetTimeout関数の完了を待たず、次に記述された処理であるCが出力されます。

上記の例から分かる通り、JavaScriptには処理の完了を待たず、次の処理に進む仕組みがあります。この仕組みが「非同期処理」と呼ばれるものです。

今回はsetTimeoutによる非同期処理を取り上げましたが、他にもファイル読み込みの非同期処理、データベースからデータを取得する非同期処理などがあります。

Promiseの具体的な記述方法

ここからは、Promiseを用いて非同期処理をどのように記述するか見ていきましょう。

Promiseオブジェクトでは、非同期処理の状態を次の3つの状態で表現します。非同期処理に成功した状態fulfilled、非同期処理に失敗した状態rejected、非同期処理の結果が未確定の状態pendingです。

まずは、Promiseを利用した非同期関数の実装例を見てみましょう。Promiseを利用した非同期関数では、非同期関数が呼び出されるとすぐにPromiseインスタンスを返却するように実装します。

async-function.js
function asyncFunction(data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() 

上の例は、引数を2倍した値を返しますが、約50%の確率でエラーになる非同期関数です。resolverejectという2つの引数をとり、new Promise((resolve, reject)と記述することで、Promiseインスタンスを生成します。

非同期処理が成功したときはresolve()を、失敗したときはreject()を使って記述します。この例では、非同期処理の成功時に、引数を2倍した値を返すようresolve(data * 2)と記述しています。

resolve()が実行されるとPromiseインスタンスの状態はfulfilledとなり、reject()が実行されるとrejectedになります。rejectを実行する際は、reject(new Error())というようにエラーオブジェクトを引数に渡します。

3種類のインスタンスメソッド

ここからは、Promiseによるエラーハンドリングを見ていきましょう。エラーハンドリングでは、Node.jsにより提供されているPromiseの3つのインスタンスメソッドを使います。

  • then
  • catch
  • finally

上記のメソッドについて、詳しい使用方法を見ていきましょう。

thenメソッド

1つ目のメソッドは、thenメソッドです。thenメソッドは、Promiseインスタンスの状態が、fulfilled、またはrejectedになったときに実行するコールバックを登録するメソッドで、次のように記述します。

sample-promise-then.js
function asyncFunction(data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random()  {
    // fulfilledになったとき実行される処理
    console.log(value); // 10
  },
  (error) => {
    // rejectedになったとき実行される処理
    console.log(error.message); // "エラーが発生しました"
  }
);

thenメソッドの第1引数には、Promiseインスタンスの状態がfulfilledになったとき実行するコールバックを登録し、第2引数には、Promiseインスタンスの状態がrejectedになったとき実行するコールバックを登録します。

thenメソッドの第1引数に登録したコールバックの引数となるvalueには、Promiseインスタンスにてresolve(value)と記述した部分の引数valueが渡されます。今回は、5 * 2の結果である10が渡されています。

thenメソッドの第2引数に登録したコールバックの引数となるerrorには、Promiseインスタンスにてreject(error)と記述した部分の引数errorが渡されます。今回は、エラーオブジェクトであるnew Error("エラーが発生しました"))が渡されています。

thenメソッドの引数に渡すコールバックについて、then(onFulfilled, onRejected)と記述すると、onFulfilledonRejectedはどちらも省略可能です。次は、thenメソッドにてonRejectedを省略したときに利用できる、catchメソッドについて説明します。

catchメソッド

catchメソッドは、次のように記述します。

sample-promise-catch.js
function asyncFunction(data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random()  {
  // fulfilledになったとき実行される処理
  console.log(value); // 10
}).catch((error) => {
  // rejectedになったとき実行される処理
  console.log(error.message); // "エラーが発生しました"
});

thenメソッドの第1引数に登録したコールバックの引数となるvalueには、Promiseインスタンスにてresolve(value)と記述した部分の引数valueが渡されます。今回の例では、5 * 2の結果である10が渡されています。

catchメソッド内のerrorには、Promiseインスタンスにてreject(error)と記述した部分の引数errorが渡されます。今回は、new Error("エラーが発生しました"))が渡されています。

繰り返しになりますが、catchメソッドを利用するためには、thenメソッドの第2引数に渡すコールバック(先の文脈におけるonRejected)を省略する必要があります。

finallyメソッド

最後は、finallyメソッドについて説明します。

finallyは、非同期処理の成功時と失敗時のどちらも実行するコールバックを登録するメソッドです。言い換えると、Promiseインスタンスの状態がfulfilledまたはrejectedになったときに実行される関数を指定できます。

finallyメソッドは、次のように記述します。

sample-promise-finally.js
function asyncFunction(data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random()  {
// fulfilledになったとき実行される処理
  console.log(value); // 10
}).catch((error) => {
// rejectedになったとき実行される処理
  console.log(error.message); // "エラーが発生しました"
}).finally(() => {
// fulfilled または rejected になったとき実行される処理
  console.log("fulfilled または rejected になったとき実行");
});

finallyは2018年の規格から導入された新しいメソッドです。ファイルを閉じる・ネットワークを接続を切断するなど、Promiseの結果にかかわらず処理の後始末をしたい際に適しています。

Lesson 6 Chapter 4
Expressの共通エラー処理

今までは、node sample.jsのようにして実行するプログラム中のエラー処理について学習してきました。Chapter4では、Node.js上で利用できるWebアプリケーションフレームワーク「Express」のエラー処理を見ていきましょう。

私たちが普段目にするタスク管理アプリやECサイトは、いずれもWebアプリケーションの一種です。Expressのエラー処理を学習することで、実践的なエラー処理のイメージを掴んでいきましょう。

デフォルトのエラー動作

まずは、Expressによる単純なアプリケーションを考えてみます。以下のファイルを作成してください。

app.js
const express = require("express");
const app = express();
const port = 3000;

app.get("/", (req, res) => {
  res.send("Hello World!");
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

node app.jsと実行してから、ブラウザでhttp://localhost:3000/にアクセスしてみましょう。Hello World!と出力されたらOKです。

Expressのインストール

Expressをインストールしていない方は、Lesson3-2「Expressのインストール」を参考に環境を構築してください。

次は、Express上でthrow文を使ってエラーを発生させてみましょう。

先ほどのapp.jsを次のように変更し、再度node app.jsと実行してから、ブラウザなどでhttp://localhost:3000/にアクセスしてみてください。

app.js
const express = require("express");
const app = express();
const port = 3000;

app.get("/", (req, res) => {
  throw new Error("エラーです");
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

今度は、添付画像のような「エラーです」というメッセージと「英数字と記号の羅列」が表示され、プログラムが終了したと思います。いったい何が起きたのでしょうか。

express-error-message.png Expressのエラー表示

すでに述べた通り、ExpressはWebアプリケーションフレームワークです。したがって、クライアントからHTTPリクエストを受け取り、何かしらの処理をしてクライアントへHTTPレスポンスを返却する、という一連の流れがあります。

Hello World!を表示した際には、クライアントからHTTPリクエストを受け取り、HTTPレスポンスと一緒にHello World!という文字列を返しました。

一方、throw文を使ってエラーを発生させたときは、サーバー内部でエラーが発生したという意味であるHTTPステータスコード500をクライアントに返し、画面上には発生したエラーの詳細を表示しています。

以上がExpressにおけるエラー発生時のデフォルト動作です。HTTPリクエストを送ってきたクライアントにはエラーが発生したことを伝え、アプリケーション開発者にはエラーの詳細を伝える、という2つ役割を果たしています。

ここで、あなた普段使っているWebアプリケーションを思い浮かべてみてください。上の添付画像のように、英数字と記号の羅列で「エラー」と表示されるアプリケーションに出会ったこと少ないでしょう。それはアプリケーションの開発者が、先に学習したtry-catch構文やPromiseメソッド、独自のErrorサブクラスなどを組み合わせて、適切にエラー処理を行っているためです。

Expressによる適切なエラー処理

次は、Expressのエラー処理を好ましい形に変更してみましょう。

先ほどのapp.jsを次のように変更し、再度node app.jsと実行してから、ブラウザなどでhttp://localhost:3000/にアクセスしてみてください。

app.js
const express = require("express");
const app = express();
const port = 3000;

app.get("/", (req, res) => {
  try {
    throw new Error("エラーです");
  } catch (error) {
    next(error);
  }
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

app.use((err, req, res, next) => {
  console.log(err.stack);
  res.status(500).send("expressのエラー処理の例です");
});

下の添付画像のように、「Expressのエラー処理の例です」と表示され、プログラムは終了していないことが確認できたと思います。

また、ブラウザの開発者ツールを開いてみると、HTTPステータスコード500と、「Expressでのエラー処理の例です」をHTML形式で受け取っていることが確認できます。以上が最も単純なExpressのエラー処理の方法です。

express-error-handling-1.png ブラウザでアクセスしたときのエラー表示

Expressにおけるエラー処理の流れ

あらためて、Expressのエラー処理の流れを詳しく見ていきましょう。以下は先ほどのコードを簡略化したものです。

expressのエラー処理の解説
app.get("/", (req, res) => {
  try {
    // A. throw文により、エラーオブジェクトが生成される
    throw new Error("エラーです");
    } catch (error) {
      // B. expreeのエラー処理関数にエラーオブジェクトを渡す
      next(error);
    }
});

// エラー処理関数の宣言
app.use((err, req, res, next) => {
  // C. next(error) により実行される処理
  res.status(500).send("expressのエラー処理の例です");

コードを見ながら、以下の3ステップで処理が進むことを確認してください。

  1. HTTPリクエストを受け取ると、try-catch構文のtry節内にてエラーオブジェクトが生成される。
  2. catch節内にて、next(error)と記述することで、Expressのエラー処理関数にキャッチしたエラーオブジェクトが渡される。
  3. エラー処理関数内にてHTTPステータスコード500と、「Expressでのエラー処理の例です」をHTML形式でHTTPレスポンスと一緒に返却される。

Expressにおけるエラー処理の注意点は次の3点です。

  • エラー処理関数を宣言するときは、4つの引数(err, req, res, next)をとって宣言する必要があります。これは、Expressでは引数が4つあることをもって、エラー処理関数と認識しているためです。
  • 発生したエラーについてnext(error)と記述する必要があります。next(error)と記述することで、宣言したエラー処理関数で処理されます。
  • エラー処理関数は、できるだけミドルウェア関数とルートハンドラを記述し終えた最後の位置に置いてください。

今回説明した(err, req, res, next)と4つの引数をとるエラー処理関数は、Expressのデフォルトエラー処理を上書きします。

あなたが今後Webアプリケーションを作成する際には、HTTPステータスコード500とHTML形式のメッセージではなく、別のHTTPステータスコードやメッセージを送りたい場面も出てくるでしょう。

そこで次の章では、エラー画面を表示させる例を見ていきましょう。

Lesson 6 Chapter 5
エラー画面の実装

Chapter5では、Expressを使ってエラー画面を表示させる方法を解説します。

本章のChapter3では、EJSを利用して静的HTMLページを作成しました。今回は、Expressにてエラーが発生した際、同様にEJSを利用してエラーページを表示するようにしてみましょう。

Hello Worldの画面を表示する

まずは、次のように記述して「Hello World!」の文字列を表示してください。

app.js
const express = require("express");
const app = express();
const port = 3000;

// テンプレートエンジンを ejs に設定
app.set("engine", "ejs");

// ejsファイルを呼び出すディレクトリを指定
app.set("views", "./views");

app.get("/", (req, res) => {
    res.render("./index.ejs");
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

Hello World!と表示させるときの注意点は次の2つです。

  • npm install ejsを実行して、EJSをインストールする。
  • viewsディレクトリを作成し、viewsディレクトリ内にindex.ejsを配置する。

Expressでは、app.set("views", "./views")と記述することで、EJSファイルを読み込むディレクトリを指定します。今回は、viewsディレクトリからEJSファイルを読み込むよう指定しています。また、viewsディレクトリ内に配置するindex.ejsには、HTML形式でHello World!と記述してください。

エラー画面を表示する

次は、エラー画面を表示させる例を見ていきましょう。また、エラー画面用に、viewsディレクトリ内には500.ejsを配置し、HTML形式で500エラーですと記述してください。

app.js
const express = require("express");
const app = express();
const port = 3000;

app.set("engine", "ejs");
app.set("views", "./views");

app.get("/", (req, res) => {
    try {
      throw new Error("エラーです");
    } catch (error) {
      next(error);
    }
});

app.listen(port, () => {
    console.log(`Example app listening on port ${port}`);
});

app.use((err, req, res, next) => {
    res.status(500);
    res.render("./500.ejs");
});

下の添付画像のように、500エラーですと表示されることが確認できたと思います。

express-error-handling-2.png 500エラーの例

今回はエラー画面を表示させましたが、処理を工夫することで再入力や再実行を促す画面の表示も可能です。

ここまで、Expressでのエラー処理を見てきました。Webアプリケーションでのエラー処理についてイメージが掴めてきたでしょうか。

次のChapterでは、より実践的に、エラーの切り分け方法やエラーの想定場面について解説します。

Lesson 6 Chapter 6
発生しうるエラーの切り分け

Lesson6では、エラー処理の具体的な記述方法を見てきました。ExpressとEJSを組み合わせることで、拡張的なエラーハンドリングが簡単に実現できます。

とはいえ、これまでのChapterで扱ってきたのは、意図的にエラーを発生させた上での処理でした。実際のアプリケーション開発では、事前に想定されるいくつもの例外に対して、適切なエラーハンドリングを行わなければなりません。

そこで今回のChapterでは、Webアプリケーションにおけるエラーの「切り分け方」を学びます。どのような場面でエラーが発生するか知っておけば、エラーハンドリングの質も向上するでしょう。

エラー処理のパターン

エラー処理の大まかな分類として「業務エラー」と「システムエラー」があります。両者の違いを知るために、簡単な電卓アプリの開発を想像してみましょう。この電卓アプリには、2つのエラーが想定されます。

ユーザーの入力・操作に起因するエラー(業務エラー)

1つ目は「業務エラー」と呼ばれるもので、ユーザーの入力・操作に起因するエラーです。たとえば、電卓アプリにひらがなを入力した場合がそれに当たります。

当然、ひらがなの計算はできないため、アプリは処理を中断し、ユーザーに再入力を促さなければなりません。

このとき大切な点は、できるだけ具体的にユーザーの行動を指し示すことにあります。ただ「エラー」と表示しただけでは、ユーザーは何が中断の原因であるのか分かりません。ひょっとすると自分のミスに気付かず、再び同じ値を入力する恐れがあります。

皆さんもWebサービスにログインしようとして、パスワードの間違いに気付かず、何度も誤入力してしまった経験があるはずです。

ユーザビリティに直結する部分でもあるため、表示するメッセージは細かく切り分けるべきでしょう。

開発者のプログラムに起因するエラー(システムエラー)

2つ目は「システムエラー」と呼ばれるもので、開発者が記述するプログラムに起因するエラーです。電卓アプリの例でいえば、ゼロ除算を許容してしまった場合に起こるエラーが該当します。

ゼロ除算とは

何らかの値を0で割ること。数学的には未定義であり、多くのプログラミング言語でエラーを起こすか、NaN(非数)の結果を返す。

この場合、エラーメッセージを表示してもその場で原因が解消されるわけではありません。問題を根本的に解決するためには、ゼロ除算を許容しないように開発者がプログラムを修正するしかないからです。

ただし、それでもユーザーに不具合の発生を伝えることには意味があります。なぜなら、エラーが発生した詳しい状況を開発者に伝えてもらえるからです。多くの場合、エラーの発生箇所や発生時刻を記したログを残します。

ログについては次のLesson7で扱います。ここでは「アプリケーションは実行した処理を記録する必要がある」という点を覚えてください。

Lesson 6 Chapter 7
どういう場合にエラー処理を記述するべきか

Chapter6では、エラーの切り分け方として「業務エラー」と「システムエラー」を紹介しました。Chapter7ではより具体的に、どのような処理をする際にエラーを記述するべきか考えていきましょう。

Webアプリケーションにおけるエラーの発生箇所

Webアプリケーションに想定されるエラーの発生箇所として、主に以下の3つが挙げられます。

  • DB・ファイルの操作エラー
  • ユーザー入力値のエラー
  • 業務ロジック上のエラー

以下で順番に見ていきましょう。

DB操作・ファイルの入出力

1つ目は、データベースの操作やファイル入出力に伴うエラーです。

たとえば、データベース操作では、テーブル定義に含まれないデータを登録しようとして処理に失敗することがあります。また、ファイル入出力処理では、指定したファイルが存在せず読み込み失敗になることがあります。

データベース操作やファイル入出力に使用される関数は、処理の失敗時にthrowによって例外を通知することがほとんどです。

try-catch構文を使って例外を検知すれば、上述のエラーは安全に処理できるでしょう。

ユーザー入力時のエラー

2つ目は、受け取る値が想定と異なる場合のエラーです。

Chapter6で例示した、数値計算をする電卓アプリを思い出しましょう。ひらがなを受け取ったときを思い浮かべてください。数値や数式以外の値を受け取ったまま処理を進めると、その後の動作は不定となります。

たとえ結果が返ってきたとしても、それは正常な値とはいえません。この場合は、値を受け取ったときに値を検査し、不適切な値のときはエラーを発生させる、という処理が必要となります

ビジネスロジック上のエラー

3つ目は、在庫切れにより注文ができない、残高が足りず決済ができないなど、ビジネスロジック上で発生するエラーです。

ビジネスロジック

アプリケーションの中でシステム固有の処理を行う部分。「業務ロジック」とも呼ばれる。たとえばECサイトであれば、商品購入や在庫管理、カード決済などの処理が当てはまる。

ビジネスロジックはシステムによって異なるため、しばしば想定外のエラーが発生します。また、単一のデータではなく、複数のデータと突き合わせることで発生するエラーも多いです。

ユーザーに対しては具体的なエラーメッセージを表示し、内容に応じた処理を実行する必要があります。

まとめ:例外処理のベストプラクティス

本章では、Node.jsやExpressにおけるエラーハンドリングを学びました。まとめとして、本章の重要なポイントを挙げておきましょう。

  • 同期処理のエラーハンドリングにはtry/catch/finallyを使う
  • 非同期処理のエラーハンドリングにはPromiseを使う
  • Express+EJSでエラー画面をカスタマイズする
  • 「業務エラー」と「システムエラー」を切り分ける
  • エラーの原因に応じて適切なメッセージを表示する

Node.js(Express)では、例外の処理を怠るとプロセス自体の異常終了を引き起こします。そうなってしまえば、アプリケーションの信頼性は大きく損なわれるでしょう。

「当たり前を疑う」ことがエラーハンドリングの基本です。多角的な視点を持ち、適切な設計やテストを行うようにしてください。