【Node.js実戦習得】番外編:なぜあんなに複雑だったの?非同期エラーの new Promise((_, reject) => …) と _ の謎を徹底解剖!

Node.js

酒本先輩!

第9回で書いたあのデバッグコードなんですけど……

// 3. 非同期エラーの修正版(Promise 化して reject を使用)
app.get('/api/debug-async-error', async (req: Request, res: Response) => {
  await new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error('非同期エラーの爆弾'));
    }, 1000);
  });
  res.json({ message: '成功' });
});

これ、ちょっとコードが複雑すぎませんか?

new Promise の中で setTimeout を呼んで、しかも引数の最初にあるアンダースコア _ って一体何なんですか!?

ふふ、熊木くん。良いところに気づいたわね!
実はこれ、非同期処理とエラーハンドリングの仕組みをわざと手動で再現した『実験用コード』 だったから複雑に見えたのよ。

 

今回は番外編として、このコードの裏側にある 2つの謎 をスッキリ解き明かしていくわね!

疑問1:なぜこんなに複雑なコードを書いたのか?

結論から言うと、「古いコールバックAPI(setTimeout)」を「現代の async/await(Promise)」の文脈に無理やり乗せるための橋渡し をしていたからなの。

app.get('/api/debug-async-error', async (req: Request, res: Response) => {
  // ① Promise のインスタンスを手動で作る
  await new Promise((_, reject) => {
    // ② 1秒後にタイマーが発火
    setTimeout(() => {
      // ③ ここで reject() を呼ぶことで「Promise を失敗(Rejected)状態」にする
      reject(new Error('非同期エラーの爆弾'));
    }, 1000);
  });

  res.json({ message: '成功' });
});

なぜわざわざこんなことを?

  1. setTimeout 単体ではエラーをキャッチできない:
    タイマーのコールバック内で普通に throw new Error() を書くと、Express の枠組みを飛び出して Node.js サーバー全体がクラッシュ(curl: (52) Empty reply from server)してしまう。
  2. reject() を呼ぶことで Promise のエラーに変換:
    reject(err) を呼ぶと、その Promise は「失敗」の状態になる。
  3. awaitasync 関数がエラーを受け取る:
    await している Promise が失敗すると、async 関数の外側に例外として正しく伝播するの。Express 5 は async 関数から飛んできた Promise のエラーを自動で集中エラーハンドラーに送ってくれるから、これで安全に 500 レスポンスを返せるようになったというワケ。

💡 補足:実務ではどう書くの?

実務では setTimeout を手動で new Promise で包む機会はめったにないわ。

 

DB 操作(Prisma)や外部 API 通信(fetchaxios)など、現代の Node.js ライブラリは最初から Promise を返すように作られているの。

// 実務のリアルなコード例(PrismaでのDB操作)
app.get('/api/users/:id', async (req: Request, res: Response) => {
  // prisma.user.findUnique は最初から Promise を返すので、単に await するだけ!
  // エラーが起きれば、自動的に Express 5 の集中エラーハンドラーへ飛ぶ
  const user = await prisma.user.findUnique({
    where: { id: Number(req.params.id) }
  });

  if (!user) {
    res.status(404).json({ error: 'ユーザーが見つかりません' });
    return;
  }

  res.json(user);
});

もし実務で「1秒待つ」ような処理を作りたいなら、共通のユーティリティ関数を作っておくのが定石よ!

// 共通ユーティリティ:指定ミリ秒待つだけのスリープ関数
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

// コントローラー側はすっきり!
app.get('/api/debug-async-error', async (req: Request, res: Response) => {
  await sleep(1000); // 1秒待つ
  throw new Error('非同期エラーの爆弾'); // async 関数内での throw なので安全!
});

疑問2:Promise((_, reject) のアンダースコア _ って何?

次は、Promise((_, reject)_ の謎を解明するわね!

 

new Promise を作るとき、アロー関数の引数には通常 2つの関数 が渡されるわよね。

  1. 第1引数:resolve (処理が成功したときに呼ぶ関数)
  2. 第2引数:reject (処理が失敗したときに呼ぶ関数)
// 本来の書き方(成功も失敗も両方使う場合)
new Promise((resolve, reject) => {
  // 成功したら resolve(データ)
  // 失敗したら reject(エラー)
});

なぜ _ と書いたのか?

今回のテストコードは「100% 失敗(エラー)させたい」という目的があったわよね。


つまり、処理の中で失敗用の reject しか使わず、成功用の resolve は一度も使わない の。

もし普通に resolve と書くとどうなるか見てみましょう。

// 使わないのに resolve と書いた場合
new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error('エラー')); // reject しか使っていない!
  }, 1000);
});

TypeScript の設定(noUnusedParameters など)によっては、resolve という変数を定義したのに一度も使っていません!」 とコンパイラに怒られてしまうことがあるの。

 

そこで、JavaScript / TypeScript エコシステムでは以下の慣習が使われているわ。

使わない変数や引数の名前には _(アンダースコア)を付けることで、TS コンパイラやリンターに「使わない変数だけど、順番(位置)合わせのために書いてあるだけだから無視してね!」と伝えることができる。

// 1. 変数名として書いてしまうと...「使ってないよ!」と警告される可能性がある
new Promise((resolve, reject) => { ... })

// 2. アンダースコアにする! ...「あ、使わない位置合わせの引数だな」と安全に無視される
new Promise((_, reject) => { ... })

new Promise の第1引数は「成功用の関数」という位置が決まっているから、第2引数の reject を受け取るためにはどうしても第1引数を書かなきゃいけない。

 

だから、「使わない第1引数を _ でお茶を濁して、第2引数の reject を受け取っていた」というのが真相よ!

番外編のまとめ

  • コードが複雑だった理由: 古いコールバックAPI(setTimeout)を Promise 化して、Express に安全にエラーを拾わせる実験コードだったから。
  • 実務での書き方: DB や外部 API 通信は最初から Promise を返すため、単純に await するだけで OK。
  • _(アンダースコア)の正体: 位置合わせのために書いている「使わない引数」であることを示す TypeScript/JS の標準的な慣習。

なるほど!

実験用に仕組みを露骨に見せるコードだったから複雑に見えたんですね。

_ の謎も解けてめちゃくちゃスッキリしました!

タイトルとURLをコピーしました