Menu
日本語

JavaScript async/awaitの使い方を徹底解説

JavaScriptのasync/awaitを実例で解説。async関数の書き方、awaitでPromiseを待つ流れ、try/catchでのエラー処理、Promise.allでの並列実行まで一気にわかります。

async/await は Promise の糖衣構文

async/await は新しい非同期処理の仕組みではありません。Promise の上に乗った糖衣構文で、非同期のコードを あたかも 同期処理のように書けるようにするものです。中身はまったく同じで、見た目だけがぐっと扱いやすくなっている、ということですね。

同じ処理を両方の書き方で並べてみましょう。

index.js
Output
Click Run to see the output here.

どちらの関数も Promise を返し、やっていることはまったく同じです。ただし async 版は .then チェーンなしで上から下へ素直に読めます。これこそが async/await の魅力です。

async は「Promise を返す関数」を示す目印

function やアロー関数、メソッドの頭に async を付けると、次の2つの効果があります。

  1. その関数は 必ず Promise を返すようになります。return した値がそのまま解決値になります。
  2. 関数の中で await が使えるようになります。
index.js
Output
Click Run to see the output here.

result は文字列そのものではなく、文字列へと解決される Promise になっている点に注目してください。greet の中には await も非同期処理も一切ないのに、async キーワードが付いているだけで戻り値は自動的に Promise に包まれます。関数が例外を投げれば、その Promise は reject されます。

await は Promise が解決するまで処理を一時停止する

async 関数の内部で await somePromise と書くと、その Promise が解決するまで関数の実行が一時停止し、解決された値が返ってきます。Promise が reject された場合は、await がそのまま例外を投げます。

index.js
Output
Click Run to see the output here.

出力順に注目してみてください。"カウントダウンを開始しました""2" より先に表示されます。これは await が一時停止させるのは async 関数の中だけで、プログラム全体を止めているわけではないからです。イベントループはそのまま回り続け、countdown の方は各 wait の Promise が解決したタイミングで再開されるわけです。

await は Promise っぽいものなら何でも受け取れます。await 42 みたいな書き方も実はOKで、Promise でない値は Promise.resolve(42) でラップされて、即座に解決されます。

try/catch でエラーハンドリング

素の Promise なら .catch() をチェーンしていくところですが、async/await を使うと拒否された Promise はそのまま例外として投げられるので、いつもの try/catch でキャッチできます。

index.js
Output
Click Run to see the output here.

同じ try/catch の中に書いた await は、すべてひとつのブロックでまとめて拾えます。ネットワークエラー、JSON のパースエラー、自分で throw した例外も、全部同じ catch に飛んでくる。ネストした .then/.catch を書き連ねていた頃と比べると、これはかなり大きな進化です。

ただし注意点がひとつ。fetch が reject するのはネットワークエラーのときだけで、HTTP の 4xx/5xx ではエラーになりません。自分で res.ok をチェックして throw する —— 実務のコードで嫌になるほど見かけるパターンです。

ループ内で安易に await しない

これは async/await で一番ハマりやすい落とし穴です。ループの中でそのまま await を書くと処理が直列になり、毎回前のイテレーションが終わるまで待つことになります。

index.js
Output
Click Run to see the output here.

sequential は900ms前後、parallel は300ms前後で終わります。基本ルールとしては、タスク同士に依存関係がなければ全部一気にキックしてから await Promise.all でまとめて待つこと。1つずつ await するのは、次の呼び出しに前の結果が本当に必要なときだけで十分です。

配列を処理するときの定番は Promise.all(items.map(async (x) => ...)) のパターンです。for...of の中で await する書き方だと直列実行になります。レート制限や順序制御のためにわざとそうしたい場面もありますが、たいていは避けたほうが無難です。

async/await と通常の Promise を組み合わせる

どちらか一方に統一する必要はありません。async 関数は Promise を返しますし、await はどんな Promise にも使えるので、自由に混ぜて書けます。

index.js
Output
Click Run to see the output here.

どちらのスタイルを使っても構いません。上から下へ素直に読ませたいときは await、サクッと単発で済ませたいときや async の外で書きたいときは .then が便利です。

トップレベル await(ES モジュール内)

以前は await をスクリプトのトップレベルで書くことができず、必ず async 関数で包む必要がありました。しかし今は状況が変わり、ES モジュール.mjs ファイルや <script type="module">)の中であれば、トップレベルで直接 await を書けるようになっています。

// ES モジュール内
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await res.json();
console.log(user.name);

トップレベルの await を使うと、awaitしている Promise が解決するまでモジュール自体の読み込みが完了しません。そのモジュールを import する側もその分待たされることになります。設定ファイルの読み込みや動的 import には便利ですが、使いどころは選びましょう。遅いトップレベル await は、そのモジュールを import するすべての箇所をブロックしてしまいます。

CommonJS のファイルや通常のインラインスクリプトでは、これまで通り SyntaxError になります。昔からある回避策は、即時実行の async 関数でくくってしまう方法です。

index.js
Output
Click Run to see the output here.

ハマりやすいポイント

よくある落とし穴をざっとまとめておきます。

  • async の付け忘れ。 普通の関数の中で await を使うと構文エラーになります。関数に async を付けるか、async な関数を呼び出して .then で受けるようにしましょう。
  • 戻り値に await を付け忘れる。 const data = getJSON(url); と書くと、受け取れるのは Promise であってデータそのものではありません。値として扱うと、出力に [object Promise] が出てきて驚くことになります。
  • キャッチされない reject。 doWork(); のように投げっぱなしで呼び出した async 関数は、.catch を付けるか try/catch の中で await しない限り、エラーを黙って握りつぶします。
  • forEach に async コールバックを渡すケース。 array.forEach(async (x) => await something(x)) は実は何も待ってくれません。forEach は返ってきた Promise を無視してしまうからです。for...ofawait を組み合わせるか、Promise.all(array.map(...)) を使いましょう。
index.js
Output
Click Run to see the output here.

実行してみてください。broken は待たずに return してしまうので、"完了" が出る前に "終わった?" が表示されます。一方の fixed はすべての処理を待ってから、最後に "終わった!" を表示します。

async/await はいつ使うべきか

非同期処理を複数ステップ順番に実行する場合や、try/catch でエラーハンドリングしたい場合は、基本的に async/await を選んでおけば間違いありません。一方で、ワンライナーで済むような単純なケース、ライブラリ内で Promise をそのまま返すだけで await する必要がないケース、あるいは Promise.race.finally() のようなコンビネータをチェーンで使いたいケースでは、素の Promise のままでも十分です。

うまく使えば、async/await で書かれた非同期コードはレシピのように読めます。これをやって、次にこれ、最後にこれ、という具合です。裏でイベントループが動いているのは変わりませんが、コールバックの入れ子から解放されるのが嬉しいところです。

次は fetch API

ここまでの例では、「何らかの非同期処理」の代役としてずっと fetch を使ってきました。せっかくなのでちゃんと取り上げておきたいところです。リクエストとレスポンスの仕組み、JSON の扱い方、ヘッダーの設定、そして fetch が HTTP エラーで reject されない理由など。次のページで解説します。

よくある質問

async/awaitって結局なにをしてくれるの?

ひとことで言うと、Promiseを同期コードっぽい見た目で書けるようにする構文です。関数にasyncを付けるとその関数は必ずPromiseを返すようになり、中でawaitを書くと指定したPromiseが決着(resolve / reject)するまでその場で一時停止して、解決された値を返してくれます。裏側で動いているのはあくまでPromiseなので、仕組みが変わるわけではなく「読みやすくなるだけ」というイメージでOKです。

async関数の外でawaitは使える?

ESモジュールのトップレベルでなら使えます。いわゆる「トップレベルawait」です。一方、普通の関数の中やCommonJSのスクリプトではawaitは構文エラーになります。対処法としては、コードをasync関数でラップして呼び出すか、ファイル自体をESモジュール(.mjs"type": "module")に切り替えるのが定番です。

async/awaitでのエラー処理はどう書く?

基本はawaitしている箇所をtry/catchで囲むだけです。awaitしたPromiseがrejectされると、それは例外としてthrowされるのでcatchブロックで拾えます。一方、awaitせずに投げっぱなしにしているバックグラウンド処理には、必ず.catch()を付けておきましょう。そうしないとunhandled rejectionになって後で痛い目を見ます。

awaitするとプログラム全体が止まる?

止まりません。awaitで一時停止するのはあくまで「そのasync関数の中」だけです。イベントループ自体は動き続けるので、タイマーは発火するし、ほかの非同期処理も進むし、UIもフリーズしません。呼び出し元にはその場でpending状態のPromiseが返り、処理はそのまま先に進みます。

Coddyでコードを学ぼう

始める