Menu

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

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

このページのコードはエディタで実行できます — 編集してすぐに結果を確認できます。

async/await は Promise の糖衣構文

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

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

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

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

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

  1. その関数は 必ず Promise を返すようになります。return した値がそのまま解決値になります。
  2. 関数の中で await が使えるようになります。

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

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

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

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

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

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

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

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

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

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

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

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 にも使えるので、自由に混ぜて書けます。

どちらのスタイルを使っても構いません。上から下へ素直に読ませたいときは 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 関数でくくってしまう方法です。

ハマりやすいポイント

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

  • 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(...)) を使いましょう。

実行してみてください。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 programming languages illustration

Coddyでコードを学ぼう

始める