Node.jsのasync/awaitでUnhandledPromiseRejectionWarningを修正する

intermediate💚 Node.js2026-03-19| Node.js v10〜v18以降、全OS(Linux、macOS、Windows)

Error Message

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1)
#nodejs#promise#async#await#error-handling

何が起きているか

Node.jsアプリがコンソールに次のようなメッセージを出力しています:

UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1)
UnhandledPromiseRejectionWarning: Error: connect ECONNREFUSED 127.0.0.1:5432
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1144:16)
(node:12345) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated.
In future versions of Node.js, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

DB接続、APIコール、ファイル読み込みなど、どこかで処理が静かに失敗しています。アプリは何事もなかったかのように動き続けます。Node.js v15以降では、これがプロセスをそのまま強制終了させます。古いバージョンでは警告を表示して処理を続行するだけで、ある意味それのほうが始末が悪いです。サイレントな失敗は、本番環境で最も追跡が難しいバグです。

なぜ起きるのか

コードのどこかでPromiseがrejectされたにもかかわらず、それをキャッチするものが存在しません。ほぼ毎回、次の3つのパターンのいずれかが原因です:

  • async関数がエラーをthrowしたが、呼び出し元がtry/catchの中でawaitしていない
  • .then()チェーンの末尾に.catch()がない
  • イベントハンドラーやコールバックがasync関数を呼び出したが、返されたPromiseを無視している

原因箇所の特定

まず有用なスタックトレースを取得します。--trace-warningsフラグを付けてNodeを実行してください:

node --trace-warnings app.js

これにより、rejectionが発生した正確な行を指す完全なスタックトレースが出力されます。このフラグがないと、古いバージョンのNodeはrejectionメッセージしか表示せず、発生元の手がかりが得られません。

別の方法として、デバッグ中はエントリファイルの先頭にグローバルハンドラーを追加することもできます:

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection at:', promise, 'reason:', reason);
});

index.jsまたはアプリの起点となるファイルの1行目に記述してください。これにより、漏れ出たすべてのrejectionをキャッチできます。問題の追跡には有効ですが、恒久的な修正として扱わないでください。

修正方法

修正1:async/awaitをtry/catchで囲む

十中八九、原因はこのような形をしています — エラーハンドリングなしで呼び出されたasync関数です:

// 問題あり — rejectionが処理されない
async function fetchUser(id) {
  const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
  return user;
}

fetchUser(123); // awaitもなく、.catch()もない

内部をtry/catchで囲み、呼び出し元でもエラーを処理します:

// 修正済み — エラーがキャッチされる
async function fetchUser(id) {
  try {
    const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
    return user;
  } catch (err) {
    console.error('Failed to fetch user:', err.message);
    throw err; // 呼び出し元に問題を伝えるために再スロー
  }
}

// 呼び出し元でも処理する
try {
  const user = await fetchUser(123);
} catch (err) {
  // ハンドリングまたはログ出力
}

修正2:Promiseチェーンに.catch()を追加する

async/awaitではなく.then()チェーンを使用している場合は、チェーンの末尾に必ず.catch()を追加してください:

// 問題あり — rejectionが静かに消える
fetch('https://api.example.com/data')
  .then(res => res.json())
  .then(data => process(data));
  // .catch()が欠けている
// 修正済み
fetch('https://api.example.com/data')
  .then(res => res.json())
  .then(data => process(data))
  .catch(err => {
    console.error('API call failed:', err.message);
  });

修正3:イベントハンドラー内のasync関数を処理する

これは意表を突かれがちなパターンです。イベントエミッターやコールバックはPromiseを理解しないため、asyncコールバック内のエラーは静かに外へ漏れ出します:

// 問題あり — Expressのルート
app.get('/users/:id', async (req, res) => {
  const user = await fetchUser(req.params.id); // throwされても?Expressには届かない
  res.json(user);
});
// 修正済み — try/catchで囲み、next()に転送する
app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await fetchUser(req.params.id);
    res.json(user);
  } catch (err) {
    next(err); // Expressのエラーハンドラーに渡す
  }
});

すべてのルートにこのボイラープレートを書くのはすぐに面倒になります。小さなラッパーユーティリティでスッキリさせましょう:

const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await fetchUser(req.params.id);
  res.json(user);
}));

修正4:安全網としてのグローバルハンドラー(本番環境)

コードベース全体でしっかりエラーハンドリングをしていても、グローバルなフォールバックを追加する価値はあります。これが最後の防衛ラインです:

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Promise Rejection:', reason);
  // モニタリングサービス(Sentry、Datadogなど)にログを送る
  // その後、必要に応じてグレースフルシャットダウンを実行
  // process.exit(1);
});

Node.js v15以降では、未処理のrejectionが発生するとデフォルトでプロセスが終了します。このハンドラーを使えば、終了前にエラーをログに記録してクリーンアップを実行する時間を確保できます。

修正の確認

アプリを起動し、警告が発生していたコードパスを意図的に実行してみましょう:

node --trace-warnings app.js
  • UnhandledPromiseRejectionWarningの行が表示されなくなっているはずです
  • エラー条件がまだ発生する場合、ログには未処理のrejectionではなく、適切にキャッチされたエラーが表示されるはずです
  • Node.js v15以降では、アプリが予期せずクラッシュしなくなるはずです

rejectionを強制的に発生させて、ハンドラーが機能することを確認するシンプルなテストを書きましょう:

// ハンドラーが機能するかを確認するためにrejectionを強制発生させる
async function test() {
  try {
    await Promise.reject(new Error('test rejection'));
  } catch (err) {
    console.log('Caught expected error:', err.message); // この行が出力されるはず
  }
}

test();

得られた教訓

すべてのasync関数呼び出しにはエラーハンドリングが必要です — 例外なく。awaittry/catchで囲むか、返されたPromiseに.catch()を付けるかのどちらかです。難しいのは、非asyncなコンテキストから呼び出されるasync関数です:イベントハンドラー、setTimeoutのコールバック、ストリームのリスナーなど。これらはどれも、あなたのrejectされたPromiseのことを気にしません。

Node.js v14以前を使用している場合、プロセスが動き続けるからといってこれらの警告を軽視しないでください。v15以降にアップグレードすれば、Nodeが強制的に対処を迫ります — 本番環境で深夜2時にサイレントなデータ破損バグを追跡するよりも、今修正するほうが賢明です。

--trace-warningsはデバッグのツールキットに常備しておきましょう。発生源が不明な警告が出た瞬間に、このフラグが数秒で見つけ出してくれます。

Related Error Notes