3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

return await は付けても付けなくても同じ? try の中では catch と finally の動きが変わる

3
Posted at

async 関数の最後で return await fetchData() と書くと、「await は要らない、return fetchData() で同じ」と言われることがあります。ESLint にも、return await を警告する no-return-await というルールがあります(今は非推奨)。

結論から書くと、try / catch の外では、呼び出し元に届く値や例外は同じです(スタックトレースの出方は変わることがあります。Q4 で扱います)。一方、try の中では、catch と finally の動きが変わります。この記事では、Node.js v24.14.1 で実際に試した結果をもとに、どこで違いが出るのかを Q&A で整理します。コードは .mjs ファイルに保存して node ファイル名.mjs で実行しています(関数の外で await を使うため)。

Q1. try の外なら、本当に同じ?

呼び出し元に届く結果は同じでした。どちらも、失敗すると呼び出し元の catch に届きます。

const fail = () =>
  new Promise((_, reject) => setTimeout(() => reject(new Error("取得失敗")), 10));

async function g1() { return fail(); }
async function g2() { return await fail(); }

try { await g1(); } catch (e) { console.log("g1:", e.message); } // g1: 取得失敗
try { await g2(); } catch (e) { console.log("g2:", e.message); } // g2: 取得失敗

Q2. try の中だと、何が変わる?

自分の関数の中の catch で拾えるかどうかが変わります。

// fail は Q1 と同じ
async function withoutAwait() {
  try {
    return fail();
  } catch (e) {
    return "catchで拾えた";
  }
}

async function withAwait() {
  try {
    return await fail();
  } catch (e) {
    return "catchで拾えた";
  }
}

console.log(await withAwait()); // catchで拾えた
await withoutAwait().catch((e) => console.log("例外:", e.message)); // 例外: 取得失敗

実行した結果です。

書き方 結果
return fail() 中の catch を素通りして、呼び出し元に例外が届いた
return await fail() 中の catch で拾えた("catchで拾えた" が返った)

try / catch が拾えるのは、try の中で投げられた例外だけです。Promise の失敗(reject)は、await したところで初めて例外になります。return fail() は Promise をそのまま返すだけで、try の中では何も投げないので、catch は動きません。await を付けると、try の中で失敗が例外になるので、catch で受け止められます。

失敗するタイミングの問題ではありません。返す時点ですでに失敗している Promise でも、同じく素通りしました。

async function bad() { throw new Error("すぐ失敗"); }

async function h() {
  try {
    return bad();
  } catch {
    return "catchで拾えた";
  }
}

h().then((v) => console.log("値:", v), (e) => console.log("例外:", e.message));
// 例外: すぐ失敗

return Promise.reject(new Error("reject済み")) でも同じ結果でした。

なお、async の付いていない普通の関数がその場で throw した場合は、try の中で例外が投げられるので、await がなくても catch で拾えました。素通りするのは、Promise の失敗を await せずに返したときです。

「エラーのときは代わりの値を返す」つもりで try / catch を書いたのに、await がないせいで例外がそのまま上に飛ぶ、という形でバグになります。

Q3. finally はどう変わる?

実行される順番が変わりました。

const log = [];
const slow = () =>
  new Promise((resolve) => setTimeout(() => { log.push("処理完了"); resolve("ok"); }, 10));

async function f1() { try { return slow(); } finally { log.push("finally"); } }
async function f2() { try { return await slow(); } finally { log.push("finally"); } }

await f1(); console.log(log); // [ 'finally', '処理完了' ]
log.length = 0;
await f2(); console.log(log); // [ '処理完了', 'finally' ]
書き方 log の順番
return slow() finally → 処理完了
return await slow() 処理完了 → finally

finally で「読み込み中」の表示を消したり、接続を閉じたりしている場合、await がないと、処理が終わる前に片付けが走ります。保存中のボタンを finally で元に戻す書き方は 保存ボタンを2回押すと、データが2件登録される で扱いました。そこでも try の中で通信を await してから、finally でボタンを戻しています。await なしで Promise を返す形にすると、通信が終わる前にボタンが押せる状態に戻ります。

Q4. try の外なら、付けないほうが速い?

速さを理由に外す必要は、今はありません。ESLint の公式ドキュメントでは、no-return-await は使わないことを勧めています(ESLint v8.46.0 で非推奨)。理由として挙げられているのは次の2つです。

  • 仕様の変更で、return await によって余分な待ち(マイクロタスク)が増えることはなくなった
  • return await のほうが、デバッグのときのスタックトレースが分かりやすい

スタックトレースの違いは、手元(Node.js v24.14.1)でも確かめられました。

async function inner() { await null; throw new Error("boom"); }
async function outerNoAwait() { return inner(); }
async function outerAwait() { return await inner(); }

for (const fn of [outerNoAwait, outerAwait]) {
  try { await fn(); } catch (e) { console.log(e.stack); }
}

q4.mjs という名前で保存して実行したときに、実際に出た内容です(1つ目が outerNoAwait、2つ目が outerAwait。フォルダの場所は省略しています)。

Error: boom
    at inner (q4.mjs:1:44)
Error: boom
    at inner (q4.mjs:1:44)
    at async outerAwait (q4.mjs:3:38)
    at async q4.mjs:6:9

outerAwait 経由で失敗したときは、エラーのスタックトレースに at async outerAwait の行が出ました。outerNoAwait 経由のときは at inner の行だけで、outerNoAwait の名前は出ませんでした。どこから呼ばれて失敗したのかを追いたいときに、この差が効きます。

なお、inner の1行目の await null を消して、待たずにすぐ失敗させた場合は、どちらにも名前が出ました。差が出たのは、inner が一度待ってから失敗した場合です。

Node.js の機能が作るエラーでも試しました。inner の中身を return await fs.promises.readFile("存在しないファイル", "utf8") に変えた場合は、同じく outerAwait 経由のときだけ名前が出ました。一方、return await fetch("http://localhost:1") に変えて通信を失敗させた場合は、どちらにも名前は出ませんでした。いつも差が出るわけではありませんが、試した範囲では、await を付けたせいで名前が消えることはありませんでした。

Q5. じゃあ、どう書けばいい?

迷ったら、async 関数の中で Promise を返すときは return await と書いておけば、try の中でも外でも意図どおりに動きます。try の外だけで await を省く書き方も間違いではありませんが、あとで try で囲んだときに書き換え忘れると Q2 のバグになります。

TypeScript を使っている場合は、typescript-eslint の return-await ルールで、try の中で await が抜けているところを見つけられます。このルールは型の情報を使うので、typescript-eslint の型情報を使う設定で有効にします。

設定によって、try の外の扱いが変わります。

  • 既定の in-try-catch: try の中では await を付けるよう指摘し、try の外の return await は外すよう指摘します
  • always: try の中でも外でも、await を付けるよう指摘します

この記事のおすすめ(迷ったら常に付ける)に合わせるなら always です。

AI が書いた async 関数を見るときの確認ポイント

  • try の中で return 何か() のように、await なしで Promise を返していないか
  • finally で後片付けをしているのに、try の中の return に await が付いていない、という組み合わせがないか
  • catch で代わりの値を返すつもりの関数で、失敗したときに本当に代わりの値が返るかを、わざと失敗させて試したか

forEach の中の async 関数の失敗を try / catch で拾えない話は、AIが書いたtry-catchで囲んだ非同期処理、なぜUnhandledPromiseRejectionが出るのか に書きました。どちらも「Promise の失敗を try で拾えるのは、try の中で await したときだけ」という同じ仕組みです。

まとめ

  • try / catch の外では、return fail() と return await fail() で、呼び出し元に届く結果は同じだった
  • try の中では、await がないと中の catch を素通りし(すでに失敗している Promise でも同じ)、finally も処理より先に走った
  • ESLint は公式ドキュメントで、no-return-await を v8.46.0 で非推奨にした理由として、return await による追加の待ちが今はないことを挙げている
  • return await のほうが、スタックトレースに呼び出し元の名前が残る場合があった(試した範囲では、付けたせいで名前が消えた例はなかった)

未経験者向けの講座を運営しています

未経験から Next.js + Supabase + Claude Code で Webアプリを公開するまで を、全24セッションで体系化した教材です。Claude Code を学習パートナーにする CLAUDE.md と学習モード(learner / developer)の設計までセットで含みます。

※ 中上級者には易しすぎる内容なので、初心者の知り合いへの紹介や社内研修の参考としてどうぞ。

3
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?