ウェブエンジニア問題集
第6章

Node.jsのエラーハンドリング — 非同期エラー・プロセスクラッシュ対策

11
この章の目次開く

ブラウザのJavaScriptでエラー処理をサボっても、被害はそのタブの中で収まります。しかしNode.jsでは事情が違います。捕まえられなかったエラーは、プロセスそのものを終了させます。サーバーであれば、そのとき処理中だった全ユーザーのリクエストが巻き添えになります。

だからNode.jsのエラーハンドリングは、「エラーメッセージを表示する」ためではなく、プロセスを守るための技術です。この章では、Node.jsで起きるエラーの種類と、それぞれの正しい捕まえ方を学びます。

学習者学習者

try...catch で囲んだはずなのに、サーバーが落ちたんです。catchって信用できないんですか…?

先生先生

catchは信用できるよ。ただし捕まえられる範囲が決まっているんだ。Node.jsのエラーは発生のしかたが4種類あって、種類ごとに捕まえ方が違う。「try...catchで全部捕まる」と思っていると、非同期のエラーがすり抜けていく。

Node.jsのエラーは4種類ある

まず全体像です。エラーの「発生のしかた」によって、受け止め方が変わります。

種類捕まえ方
同期エラーJSON.parse の失敗、throwtry...catch
error-firstコールバックfs.readFile(path, cb) の失敗コールバックの第1引数 err を確認
Promiseの拒否(rejection)await fetch() の失敗try...catch + await、または .catch()
error イベントサーバーやストリームの異常.on('error', handler) で購読

順番に見ていきます。

同期エラー — try...catchで捕まえる

同期的に投げられたエラーは、素直に try...catch で捕まります。代表例が JSON.parse です。

try {
  const config = JSON.parse(rawText); // 不正なJSONだとSyntaxErrorをthrowする
  console.log(config.port);
} catch (err) {
  console.error('設定ファイルが壊れています:', err.message);
}
js

自分でエラーを発生させるときは throw を使います。このとき、throwするのは必ず Error オブジェクト(またはそのサブクラス)にします。throw '失敗しました' のような文字列のthrowは、スタックトレースが付かず原因調査を困難にします。

// ⭕ Errorオブジェクトをthrowする(どこで起きたかスタックトレースが残る)
throw new Error('在庫が不足しています');
 
// ❌ 文字列のthrow(スタックトレースなし。捕まえる側の扱いも困る)
throw '在庫が不足しています';
js

error-firstコールバック — 第1引数を必ず見る

Node.jsの古くからの非同期APIは、コールバックの第1引数にエラーを渡すという規約(error-firstコールバック)で統一されています。

const fs = require('node:fs');
 
fs.readFile('data.txt', 'utf8', (err, data) => {
  if (err) {
    // 失敗時: errにErrorオブジェクトが入り、dataはundefined
    console.error('読み込み失敗:', err.message);
    return; // ここでreturnしないと、下の処理が失敗時にも実行されてしまう
  }
  // 成功時: errはnull
  console.log(data);
});
js

重要なのは、コールバック内のエラーは外側のtry...catchでは捕まらないことです。コールバックが実行されるのは、tryブロックの実行がとっくに終わった後だからです(この仕組みは非同期処理とイベントループで学んだとおりです)。

// ❌ この書き方に意味はない: readFileのエラーはcatchに届かない
try {
  fs.readFile('data.txt', 'utf8', (err, data) => {
    // エラーはこの err に来る。外のcatchには来ない
  });
} catch (err) {
  // ファイルが存在しなくても、ここには到達しない
}
js

Promiseのエラー — awaitとtry...catchの組み合わせ

Promiseベースの非同期処理では、失敗は「Promiseの拒否(rejection)」として表現されます。await と組み合わせると、同期コードと同じ見た目の try...catch で捕まえられます。

const { readFile } = require('node:fs/promises');
 
async function loadConfig() {
  try {
    const raw = await readFile('config.json', 'utf8');
    return JSON.parse(raw); // 同期エラー(JSON.parse)も同じcatchで捕まる
  } catch (err) {
    console.error('設定の読み込みに失敗:', err.message);
    return defaultConfig; // フォールバック値を返して回復する
  }
}
js

await + try...catch の便利な点は、非同期エラーと同期エラーを同じcatchで受けられることです。上の例では readFile の失敗(非同期)も JSON.parse の失敗(同期)も同じcatchに届きます。

最重要のハマりどころ — awaitを忘れると捕まらない

// ❌ awaitがないため、失敗はこのtry...catchをすり抜ける
try {
  saveLog(message); // Promiseを返す関数だが、待っていない
} catch (err) {
  // saveLogの失敗はここに来ない
}
 
// ⭕ awaitすればcatchに届く
try {
  await saveLog(message);
} catch (err) {
  console.error('ログ保存失敗:', err.message);
}
js
try...catch がPromiseのエラーを捕まえられるのは、await している場合だけ——「awaitのつけ忘れ」はNode.jsのエラーすり抜けの最頻出原因です。

プロセスを落とすエラー — 最後の防波堤

ここまでの手段で捕まえ損ねたエラーは、どうなるのでしょうか。

  • 同期エラーを誰も捕まえなかった場合 → uncaught exception としてプロセスが終了する
  • Promiseの拒否を誰も処理しなかった場合 → unhandled rejection として、現在のNode.jsでは同様にプロセスが終了する
慌てて走る人
捕まえ損ねたエラー1つで、プロセス全体が終了する

process オブジェクトのイベントで、この「最後の瞬間」を捕捉できます。

構文: process.on(event, listener)

引数渡せるもの説明
event(第1引数)文字列イベント名。'uncaughtException' / 'unhandledRejection' など
listener(第2引数)関数イベント発生時に呼ばれる関数。エラーオブジェクトが渡される

戻り値: process 自身(メソッドチェーンできる)

process.on('uncaughtException', (err) => {
  // 最後のログを書き出してから、プロセスを終了させる
  console.error('致命的なエラー:', err);
  process.exit(1);
});
 
process.on('unhandledRejection', (reason) => {
  console.error('未処理のPromise拒否:', reason);
  process.exit(1);
});
js

ここで重要な設計判断があります。

uncaughtException を捕まえて「処理を継続する」のはNGです。ログを記録して、プロセスを終了させるのが正しい使い方です。

理由は、uncaught exceptionが起きた時点でアプリケーションの内部状態が壊れている可能性があるからです。書き込み途中のファイル、中途半端なDBトランザクション、解放されないリソース——この状態で動き続けると、クラッシュより厄介な「静かにデータを壊すサーバー」になります。Node.js公式ドキュメントも「クラッシュ前の同期的なクリーンアップ用であり、通常運転への復帰用ではない」と明言しています。

学習者学習者

でも本番サーバーが落ちたままだと大問題ですよね?終了させて、その後はどうするんですか?

先生先生

いい質問。答えは「外側の仕組みに再起動させる」だよ。PM2のようなプロセスマネージャやDocker、Kubernetesには、プロセスが終了したら自動で再起動する機能がある。「壊れたら綺麗な状態で再スタート」の方が、壊れたまま動き続けるより安全なんだ。この運用は本番運用の章で詳しく扱うよ。

予期するエラーと予期しないエラーを区別する

エラーの捕まえ方が分かったら、次は「捕まえた後どうするか」です。判断の軸は、そのエラーが予期できる運用上のエラー(operational error)か、バグ(programmer error)かの区別です。

分類対応
予期するエラーファイルが存在しない、接続タイムアウト、不正な入力値その場で処理する(リトライ、フォールバック、利用者にエラーを返す)
バグundefined のプロパティ参照、型の間違い、ロジックの矛盾捕まえて隠さない。ログを残してクラッシュさせ、コードを直す

「ファイルがないかもしれない」は正常な運用の一部なので、コードで回復方法を用意します。一方「バグ」をcatchで握りつぶすと、不具合が隠れたまま蓄積します。すべてのエラーを一律にcatchして無視するのは、エラーハンドリングではなく証拠隠滅です。

// ⭕ 予期するエラーだけを選んで回復し、それ以外は投げ直す
try {
  const raw = await readFile('cache.json', 'utf8');
  return JSON.parse(raw);
} catch (err) {
  if (err.code === 'ENOENT') {
    return null; // 「キャッシュファイルがまだ無い」は想定内。nullで正常続行
  }
  throw err; // それ以外(権限エラーやバグ)は隠さず上位へ
}
js

Node.jsのシステムエラーには err.code'ENOENT'(ファイルが存在しない)、'EACCES'(権限がない)、'ECONNREFUSED'(接続拒否)のような機械判定できるコードが入っています。メッセージ文字列ではなく、このコードで分岐するのが定石です。

よくあるハマりどころ

空のcatchでエラーを握りつぶす

// ❌ エラーが起きた事実ごと消える
try {
  await saveUserData(user);
} catch (err) {}
js

「とりあえず落ちないように」の空catchは、障害を静かに進行させます。最低でもログには残し、対処できないエラーは投げ直します。

errorイベントの購読漏れ

HTTPサーバーやストリームなどのイベントベースのオブジェクトは、異常を error イベントとして通知します。このイベントは、リスナーが1つも登録されていないとエラーがthrowされてプロセスが終了するという特別なルールを持っています。長時間動くオブジェクトには .on('error', handler) を必ず付けます(イベントの仕組み自体は、EventEmitterの章で詳しく扱う予定です)。

const server = require('node:http').createServer(handler);
 
// ⭕ これが無いと、ポート使用中(EADDRINUSE)などでプロセスごと落ちる
server.on('error', (err) => {
  console.error('サーバーエラー:', err.message);
});
js

catchした後、returnし忘れて処理が続行する

error-firstコールバックの例で見たとおり、「エラー処理をしたのに、その下の成功時の処理も実行されてしまう」のは典型的なバグです。エラー処理の最後に return があるか、成功パスと失敗パスが確実に分岐しているかを確認しましょう。

この章のまとめ

  • Node.jsでは捕まえ損ねたエラー1つでプロセス全体が終了する——エラーハンドリングはプロセスを守る技術
  • エラーの発生元は4種類:同期(try...catch)、error-firstコールバック(第1引数)、Promise拒否(await + try...catch)、errorイベント(.on('error'))
  • Promiseのエラーは await していないと捕まらない——「awaitのつけ忘れ」に最大限の注意を
  • uncaughtException / unhandledRejection はログを残して終了するための最後の防波堤。処理継続には使わない
  • 「予期するエラー」は err.code で判別してその場で回復、「バグ」は隠さずクラッシュさせて直す

次章では、この章でも登場した error イベントの土台である、Node.jsのイベント駆動の仕組み——EventEmitterを学びます。

参考リンク

Node.jsクイズに挑戦するこの章で学んだNode.jsの知識を、4択クイズでアウトプットして定着させよう