Lesson 15
TODOアプリのエラーハンドリング
目次
Lesson 15
Chapter 1
ロギング処理の実装を行う
このレッスンではログの処理とエラーへの対処方法について学んでいきます。 ログはバグへの対処への重要な手がかりとなり、またエラー対処はサーバを安定稼働するために重要です。 タスクを処理するコードも重要ですが、このレッスンで学ぶことも同じくらい重要です。 正しいログの取りかた、エラーハンドリングの手法について理解を深めてください。
ログとロギングについて
ログとはシステムやアプリケーションの動作状況を記録したもので、デバッグやトラブルシューティング、パフォーマンスの改善などに役立ちます。 一般的には、日時、実行結果、エラーメッセージ、アクセス情報などが記録されます。
ロギング処理とは、アプリケーションが動作する過程で発生する情報を記録する処理のことです。 主に、アプリケーションが何をしているか、何が起こっているかを追跡し、問題が発生した場合にトラブルシューティングを支援するために使用されます。
TODOアプリのログについて
今回はタスクの作成、更新、削除が行われた際にログを取っていきます。 データベースへ変更が加わる時に何が起きていたのか記録する事で、 システムがどのように動作したかを把握し、 問題が発生した場合に原因の特定や復旧に役立てることができます。 また、データの変更履歴を把握することで、何を変更したかを追跡することができます。
今回はcontroller.js内に実際にログを取るlogCreate関数を用意し、 作成更新削除それぞれの関数からこの関数を呼び出し、 引数を記録していくといった方針でログを取ります。
ロギング処理の実装を行う
controller.jsに下記を追加します。
controller.js
export const logCreate = async (taskId, action) => {
const sql = 'INSERT INTO log (task_id, action, created_at, updated_at, by_using)\
VALUES(?, ?, NOW(), NOW(), true);';
const data = await new Promise((resolve) => {
con.query(sql, [taskId, action], (err, result, fields) => {
if (err) throw err;
resolve(result);
});
});
return data;
};
引数はtaskIdとactionを持つエクスポートされた関数を定義しています。 SQLはlogテーブルに引数のデータに加え時刻等を持つレコードをインサートするものです。
この関数でログのデータをデータベースへ記述していきます。
タスクid、何があったか(action)、時刻等を記録しています。
TODO登録処理をロギングする
controller.jsのcreate関数のreturnの直前に下記を追記します。
controller.js
export const create = async (title, detail) => {
const sql = 'INSERT INTO task (id, title, detail, created_at, updated_at, by_using)\
VALUES(?, ?, ?, NOW(), NOW(), true);';
const id = uuidv4();
const data = await new Promise((resolve) => {
con.query(sql, [id, title, detail], (err, result, fields) => {
if (err) throw err;
resolve(result);
});
});
await logCreate(id, 'create'); // ここです
return data;
};
タスクidと、action引数に'create'という文字列を持たせてログ関数を呼び出しています。 タスクが作成されるたびにログ関数が動きログが記録されていきます。
TODO更新処理をロギングする
edit関数にもreturnの直前に下記を追加します。
controller.js
await logCreate(id, 'edit');
作成ログと同様にタスクが更新されるたびにログ関数が周りログが記録されていきます。
TODO削除処理をロギングする
destroy関数も同様にreturnの直前に下記を追加します。
controller.js
await logCreate(id, 'destroy');
これでロギングができるようになりました。
ログ出力をする
ログ出力についても見ていきましょう。controller.jsに追記してください。
controller.js
export const logGetAll = async () => {
const sql = 'SELECT * FROM log WHERE by_using = 1';
const data = await new Promise((resolve) => {
con.query(sql, (err, result, fields) => {
if (err) throw err;
resolve(result);
});
});
return data;
};
これは愚直にlogテーブルの中身を出力するSQLを実行しています。console.log(await logGetAll())といった形で実行すると結果として下記のようなが得られると思います。(実行前にロギングの実装を行った上でタスクの作成/編集/削除のいずれかを行ってください。)
mysql 返り値
[
RowDataPacket {
task_id: '318e1564-c831-44c5-a387-b4e1aff46778',
action: 'create',
created_at: 2023-03-17T04:23:13.000Z,
updated_at: 2023-03-17T04:23:13.000Z,
by_using: 1
},
]
タスクに何があったのかはわかりますがこれではタイトルなどがわからず少し見にくいです。SQLの機能としてJOINというものがあります。 複数のテーブルを結合して1つのテーブルとして出力するための機能です。 JOINを使用すると、1つのテーブルには存在しない他のテーブルのデータを組み合わせて、より包括的な情報を取得できます。 今回はlogテーブルとtaskテーブルをidを使用して結合してみます。logGetAll関数のsql定数を下記のように編集してください。
controller.js
SELECT * FROM log JOIN task ON log.task_id = task.id;
これはlogテーブルとtaskテーブルを、logのtask_id
とtaskのid
をキーとして結合しています。これを実行すると下記のような結果が得られます。
mysql 返り値
[
RowDataPacket {
task_id: '318e1564-c831-44c5-a387-b4e1aff46778',
action: 'create',
created_at: 2023-03-17T04:23:13.000Z,
updated_at: 2023-03-17T04:23:29.000Z,
by_using: 0,
id: '318e1564-c831-44c5-a387-b4e1aff46778',
title: 'sample_task',
detail: 'sample_task'
}
]
idをキーとして、二つのテーブルのデータが統合されわかりやすく表示できています。
これでタスクに何の変更がされたか、またそのタスクの内訳を一度に見れるようになりました。
JOINについて
JOINは、複数のテーブルから必要なデータを取得するためのSQLクエリです。 JOINを使用することで、異なるテーブルにまたがる関係を持つデータを1つの結果セットに結合することができます。 異なるテーブルのデータを結合しより詳細な情報を取得することができます。 ただし、JOINの使用には注意が必要で、複雑なクエリを作成する可能性があるためパフォーマンスの問題が発生する可能性があります。
データの正規化について
正規化はデータベース設計の一つで、重複や矛盾を排除しデータの整合性を保つための作業です。 主に第一正規化から第三正規化まであり、依存関係による矛盾を防ぎます。
例えば今回のログに関して、ログテーブルにはタスクidを入れますがタスクのタイトルなどは入れていません。 これは今後タスクのタイトルや内容が更新された時、ログテーブルに記録されたタイトルと整合性が取れなくなるからです。 idをキーとしてログテーブルとタスクテーブルをjoinする形式であればデータの整合性が取れなくなることはありません。
正規化によってデータベースに保存されるデータの整合性やデータの効率的な検索が実現されます。 しかし、正規化を過度に行うとデータの取得が複雑になる場合があるため設計の際にはバランスを考慮する必要があります。
ログを出力する
ログの出力を考えていきます。main.jsに下記を追加してください。
main.js
app.get('/api/logs', async (req, res) => {
res.send(await controller.logGetAll());
});
/api/logsにgetリクエストが来るたびにcontroller.logGetAll関数を回し、 ログの一覧を取得しています。 logGetAll関数の返り値をレスポンスしています。
ejs簡易フロント用コードは下記のようになります。
main.js
app.get('/logs', async (req, res) => {
res.render('logs', { logs: await controller.logGetAll() });
});
logGetAll関数のデータとlogs.ejsを使ってレンダリングするものです。
viewsにlogs.ejsを作成して下記を記入してください。
logs.ejs
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TODO_logs</title>
</head>
<body>
<h3>ログ一覧</h3>
<table>
<tr>
<th>id</th>
<th>内容</th>
<th>日時</th>
<th>タスクタイトル</th>
<th>タスクへ</th>
</tr>
<% for (var i = 0; i < logs.length; i++) { %>
<tr>
<td><%= logs[i].task_id %></td>
<td><%= logs[i].action %></td>
<td><%= logs[i].created_at.toJSON() %></td>
<td><%= logs[i].title %></td>
<td>
<a href="/task/<%= logs[i].task_id %>">タスクへ</a>
</td>
</tr>
<% } %>
</table>
<a href="/">トップへ</a>
</body>
</html>
これはtasks.ejsと同じ機能で構成されており新しい機能は出てきていません。 logsデータを含めてfor文で回しtableタグを埋めてレンダリングを行っています。

Lesson 15
Chapter 2
TODO 登録処理をエラーハンドリングする
エラーハンドリングとは、アプリケーション内で発生したエラーを捕捉し、適切な処理を行うことを指します。 エラーハンドリングを行うことで、予期しない動作をしてしまうことやアプリケーションがクラッシュすることを防ぎ、 ユーザーにエラーメッセージを適切に表示することができます。
具体的には、ソフトウェアが実行中に例外が発生した場合にその例外をキャッチして適切に処理することが必要です。 エラーハンドリングを適切に行わない場合、プログラムは異常終了したり期待しない挙動をしたりする可能性があります。
また、ユーザーにエラーメッセージを表示するためのフロントエンド処理や、 サーバーサイドでのエラー通知のための通知システムなども必要になることがあります。
TODO登録処理では下記のようなコードを書きました。
main.js
await controller.create(req.body.title, req.body.detail);
res.send('作成しました' + TOP_LINK);
これはエラーハンドリングがなされていないコードということになります。
このとき、万が一controller.createが異常終了するとres.send
が実行されず、クライアントはレスポンスを永遠に待ち続けてしまう状態に陥ります。
それを防ぐために使うのがtry-catch文です。
try-catchについて
tryブロック内で例外が発生する可能性があるコードを実行し、例外が発生した場合はcatchブロックにジャンプして例外を処理します。 具体的には、tryブロック内で例外が発生するとその例外がthrowされます。 throwされた例外はtryブロックの実行を中断し、catchブロックに制御を移します。 catchブロックではthrowされた例外を受け取り、処理することができます。 ただしtry-catchは便利である反面オーバーヘッドが大きく、平たく言えば処理が重いです。 巨大なtryブロックなどはサーバに負荷をかけてしまいますので注意が必要です。かといってtry-catchを使わずにサーバがエラーを起こしては本末転倒です。必要最小限度で使っていくようにしていきましょう。
次のようなコードがあると仮定します。
JavaScript
console.log('start');
try {
// 例外が発生する可能性のあるコード
} catch (e) {
console.log('error');
console.log(e)
}
console.log('end')
このようなコードがあった時、例外が発生しなければ'start'と'end'が出力されます。例外が発生した場合は、'start''error''エラー内容''end'が出力されます。仮にtry-catchを使用せずに例外を発生させた場合は例外の場所で終了し'end'は実行されません。
今回のコードをtry-catchで囲んでみます。
main.js
try {
await controller.create(req.body.title, req.body.detail);
res.send("作成しました" + TOP_LINK);
} catch (e) {
res.status(500).send("エラー: 作成に失敗しました" + TOP_LINK);
}
このようにtry-catchで囲むことでcontrollerでエラーが発生しても作成に失敗した旨をres.send
しブラウザに伝えることができます。
今回はエラーのレスポンスですのでステータスコードは500番であることを明示します。これまで明示していませんでしたが、その場合は成功を表す200番が適応されます。 他には404 Not Found(リソースが存在しないエラー)などが有名です。適切なレスポンスコードを返すように心がけましょう。
レスポンスコードはこちらのサイトにまとまっていますので参考にしてください。

Lesson 6
Chapter 3
TODO 一覧取得処理をエラーハンドリングする
タスク一覧表示は現状以下のようになっています。
main.js
app.get('/api/tasks', async (req, res) => {
res.send(await controller.getAll());
});
main.js
app.get("/tasks", async (req, res) => {
res.render("tasks", { tasks: await controller.getAll() });
});
これはエラーハンドリングがなされていないコードということになります。
このとき、万が一controller.getAllが異常終了するとres.send
やres.render
が実行されず、クライアントはレスポンスを永遠に待ち続けてしまう状態に陥ります。
それを防ぐためにtry-catch文で囲みます。
main.js
app.get('/api/tasks', async (req, res) => {
try {
res.send(await controller.getAll());
} catch {
res.status(500).send('エラー: 取得に失敗しました' + TOP_LINK);
}
});
main.js
app.get("/tasks", async (req, res) => {
try {
res.render("tasks", { tasks: await controller.getAll() });
} catch {
res.status(500).send("エラー: タスク取得に失敗しました" + TOP_LINK);
}
});
このようにtry-catchで囲むことでcontrollerでエラーが発生しても作成に失敗した旨をres.send
しブラウザに伝えることができます。
今回もHTTPステータスコードは500番エラーを明示してレスポンスしています。
今回はcatch文でエラーメッセージの引数を受け取らないスタイルで記述しました。古い環境ですと引数がないとエラーになる場合がありますが、ES2019より新しい環境であれば引数を省略できます。

Lesson 15
Chapter 4
TODO 更新処理をエラーハンドリングする
現状のタスク編集のコードはこの通りです。
main.js
app.put("/api/task/:id", async (req, res) => {
await controller.edit(req.params.id, req.body.title, req.body.detail);
res.send("更新しました");
});
main.js
app.get("/edit/:id", async (req, res) => {
res.render("edit", { task: (await controller.get(req.params.id))[0] });
});
これはエラーハンドリングがなされていないコードということになります。
このとき、万が一controller.editやcontroller.getが異常終了するとres.send
が実行されず、クライアントはレスポンスを永遠に待ち続けてしまう状態に陥ります。
それを防ぐためにtry-catch文で囲みます。
これをtry-catchで囲むとこのようになります。
main.js
app.put("/api/task/:id", async (req, res) => {
try {
await controller.edit(req.params.id, req.body.title, req.body.detail);
res.send("更新しました");
} catch {
res.status(500).send("エラー: 更新に失敗しました" + TOP_LINK);
}
});
main.js
app.get("/edit/:id", async (req, res) => {
try {
res.render("edit", { task: (await controller.get(req.params.id))[0] });
} catch {
res.status(500).send("エラー: タスク取得に失敗しました" + TOP_LINK);
}
});
このようにtry-catchで囲むことでcontrollerでエラーが発生しても作成に失敗した旨をres.send
しブラウザに伝えることができます。
今回もHTTPステータスコードは500番エラーを明示してレスポンスしています。

Lesson 6
Chapter 5
TODO 詳細取得をエラーハンドリングする
現状のコードは以下の通りです。
main.js
app.get('/api/task/:id', async (req, res) => {
res.send((await controller.get(req.params.id))[0]);
});
main.js
app.get("/task/:id", async (req, res) => {
res.render("task", { task: (await controller.get(req.params.id))[0] });
});
これはエラーハンドリングがなされていないコードということになります。
このとき、万が一controller.getが異常終了するとres.send
やres.render
が実行されず、クライアントはレスポンスを永遠に待ち続けてしまう状態に陥ります。
それを防ぐためにtry-catch文で囲みます。
これをtry-catchで囲むと以下のようになります。
main.js
app.get('/api/task/:id', async (req, res) => {
try {
res.send((await controller.get(req.params.id))[0]);
} catch {
res.status(500).send('エラー: 取得に失敗しました' + TOP_LINK);
}
});
main.js
app.get("/task/:id", async (req, res) => {
try {
res.render("task", { task: (await controller.get(req.params.id))[0] });
} catch {
res.status(500).send("エラー: タスク取得に失敗しました" + TOP_LINK);
}
});
このようにtry-catchで囲むことでcontrollerでエラーが発生しても作成に失敗した旨をres.send
しブラウザに伝えることができます。
今回もHTTPステータスコードは500番エラーを明示してレスポンスしています。

Lesson 6
Chapter 6
TODO 削除処理をエラーハンドリングする
現状のコードは以下の通りです。
main.js
app.delete("/api/task/:id", async (req, res) => {
await controller.destroy(req.params.id);
res.send("削除しました");
});
これはエラーハンドリングがなされていないコードということになります。
このとき、万が一controller.destroyが異常終了するとres.send
が実行されず、クライアントはレスポンスを永遠に待ち続けてしまう状態に陥ります。
それを防ぐためにtry-catch文で囲みます。
main.js
app.delete("/api/task/:id", async (req, res) => {
try {
await controller.destroy(req.params.id);
res.send("削除しました");
} catch {
res.status(500).send("エラー: 削除に失敗しました" + TOP_LINK);
}
});
このようにtry-catchで囲むことでcontrollerでエラーが発生しても作成に失敗した旨をres.send
しブラウザに伝えることができます。
今回もHTTPステータスコードは500番エラーを明示してレスポンスしています。

Lesson 15
Chapter 7
ログ取得をエラーハンドリングする
現状のエラー取得のコードは以下の通りです。
main.js
app.get('/api/logs', async (req, res) => {
res.send(await controller.logGetAll());
});
main.js
app.get("/logs", async (req, res) => {
res.render("logs", { logs: await controller.logGetAll() });
});
これはエラーハンドリングがなされていないコードということになります。
このとき、万が一controller.logGetAllが異常終了するとres.send
やres.render
が実行されず、クライアントはレスポンスを永遠に待ち続けてしまう状態に陥ります。
それを防ぐためにtry-catch文で囲みます。
main.js
app.get('/api/logs', async (req, res) => {
try {
res.send(await controller.logGetAll());
} catch {
res.status(500).send('エラー: 取得に失敗しました' + TOP_LINK);
}
});
main.js
app.get("/logs", async (req, res) => {
try {
res.render("logs", { logs: await controller.logGetAll() });
} catch {
res.status(500).send("エラー: エラー取得に失敗しました" + TOP_LINK);
}
});
このようにtry-catchで囲むことでcontrollerでエラーが発生しても作成に失敗した旨をres.send
しブラウザに伝えることができます。
今回もHTTPステータスコードは500番エラーを明示してレスポンスしています。
これでTODOアプリは完成です。お疲れ様でした。
