こんにちは、Ryohei(@ityryohei)です!

従来のJavaScriptでは、API通信などの非同期処理を行う際、Promise.then().catch()を多重に繋ぐ、いわゆる「コールバック地獄(Callback Hell)」に陥り、コードの可読性が著しく低下しがちでした。

しかし、ES2017で導入されたAsync/Await構文は、この課題を一気に解決し、非同期処理をあたかも同期処理のようにシンプルに記述することを可能にしました。

本記事では、このAsync/Awaitの基本的な使い方から、エラーハンドリングを含むPromiseとの連携テクニックまでを徹底解説し、あなたの非同期処理コードを劇的にクリーンにする方法を紹介します。

複数のAPIを順番に呼び出す非同期処理を書くと、ネストが深くなってコードが読みにくくなるんだ。もっとすっきり書ける方法はないかな?

上記の疑問にお答えします。

では、解説していきます。

PromiseとAsync/Awaitの関係性

なぜ従来のPromise構文は読みにくいのか

Promise自体は非同期処理の結果(成功/失敗)を扱う優れた仕組みですが、複数のPromiseを順番に実行したい場合、以下のように.then()が連鎖する形になります。

JavaScript

fetchUser('/users/1')
  .then(user => {
    return fetchPosts(user.id);
  })
  .then(posts => {
    return updateDOM(posts);
  })
  .catch(error => {
    console.error('処理中にエラーが発生:', error);
  });

処理の流れを追うためにコードの深いネストを辿る必要があり、特に複雑な条件分岐が加わると、これが可読性を大きく損なう原因となっていました。

Async/Awaitのより深い原理

Async/Awaitは、Promiseの上に構築されたシンタックスシュガー(構文糖)であり、その背後ではGenerator関数の仕組みを利用しています。

  • async関数は、実際にはPromiseを返すGenerator関数として動作します。
  • awaitキーワードに到達すると、その関数の実行は一時停止(サスペンド)されます。
  • awaitが待機しているPromiseが解決されると、ブラウザのJavaScript実行環境(エンジン)が、一時停止していた関数を再開(レジューム)させます。

つまり、awaitは実行をブロックしているのではなく、単に処理を別の場所に「委譲」し、結果が返ってきたら「再開」しているのです。これにより、開発者は同期的な感覚で非同期処理を記述できます。

基本的な使い方!非同期関数の定義と値の取得の2ステップ

Async/Awaitの使い方は非常にシンプルで、Promiseを扱う際の「待つ」という行為を明示的に記述するだけです。

ステップ1:async関数の定義

非同期処理を内部で実行する関数には、必ずasyncを付けます。

JavaScript

// asyncを付けた関数は必ずPromiseを返す
async function fetchData() {
  // ここで await を使えるようになる
  console.log('非同期処理を開始します。');
  return '完了';
}

// 呼び出すと、Promiseとして処理される
fetchData().then(result => console.log(result)); // -> 完了

ステップ2:awaitでPromiseの結果を待つ

awaitは、右辺のPromiseが解決(Resolve)されるまで待機し、結果の値を取り出します。

JavaScript

async function fetchGreeting() {
  const promise = new Promise(resolve => {
    setTimeout(() => resolve('Hello World!'), 1000);
  });
  
  // promiseが解決される(1秒後)までここで待機する
  const result = await promise; 
  console.log(result); // -> "Hello World!"
}

fetchGreeting();

実践!複数の非同期処理の直列実行

Promiseの連鎖(.then())で実現していた複数のAPI呼び出しを、Async/Awaitを使って、非常に平易なコードで直列に実行してみましょう。

エラーハンドリング:try...catchの利用

Async/Awaitの最大のメリットの一つは、Promiseのエラー(Reject)を、同期処理と同じtry...catch構文で処理できる点です。

JavaScript

// ユーザーデータとポストデータを順番に取得する非同期関数
async function fetchUserAndPosts(userId) {
  try {
    // 1. ユーザーデータを取得
    const userResponse = await fetch(`/api/users/${userId}`);
    // HTTPエラー(404など)をキャッチ
    if (!userResponse.ok) {
      throw new Error(`ユーザー取得エラー: ${userResponse.status}`);
    }
    const user = await userResponse.json();

    // 2. ユーザーの投稿データを取得
    const postResponse = await fetch(`/api/posts?userId=${user.id}`);
    const posts = await postResponse.json();

    console.log(`ユーザー名: ${user.name}`);
    console.log(`投稿数: ${posts.length}`);
    return posts;

  } catch (error) {
    // 処理の途中で発生した全てのエラーをここでキャッチ
    console.error('データ取得に失敗しました。', error.message);
    return null; // 失敗時はnullを返すなど、後続処理への対応を行う
  }
}

// 実行例
fetchUserAndPosts(1);

Promiseの.catch()を使うよりも、通常のJavaScriptと同じようにエラー処理を行えるため、コード全体の見通しが格段に良くなりました。

応用活用!並列処理のエラー耐性を高める

複数のPromiseを並列で実行する

awaitは処理を停止させるため、直列に実行されますが、Promise.all()と組み合わせることで、非同期処理を並列で実行し、パフォーマンスを向上させることができます。

JavaScript

async function fetchAllData() {
  // ユーザーと設定の取得を同時に実行し、両方の結果を待つ
  const [user, settings] = await Promise.all([
    fetch('/api/user').then(res => res.json()),
    fetch('/api/settings').then(res => res.json())
  ]);

  console.log('ユーザーデータと設定データを並列で取得完了。');
  console.log(user, settings);
}

Promise.allSettled()によるエラー耐性

Promise.all()は、実行されるPromiseのうちどれか一つでも失敗(Reject)すると、即座に全体が失敗します。

実務では、一部のデータ取得に失敗しても、成功したデータだけは使いたいケースがあります。この問題は、ES2020で導入されたPromise.allSettled()で解決できます。

JavaScript

async function fetchResilientData() {
  // 失敗するPromiseを含む
  const results = await Promise.allSettled([
    fetch('/api/user').then(res => res.json()),
    fetch('/api/fail').then(res => res.json()), // このPromiseが失敗すると仮定
    fetch('/api/settings').then(res => res.json())
  ]);

  // 結果は全てオブジェクトの配列として返される(失敗しても処理は継続)
  const successfulData = results
    .filter(result => result.status === 'fulfilled')
    .map(result => result.value);

  console.log('成功したデータのみ:', successfulData);
  // 失敗したPromiseの情報も確認可能
  console.log('全ての結果:', results); 
}

Promise.allSettled()を使うことで、並列処理中にエラーが発生しても処理が中断されず、全てのPromiseの完了ステータスを安全に確認できます。

補足:ブラウザ対応と注意点

Async/Awaitのブラウザ対応とPolyfill

Async/AwaitはES2017(ES8)で標準化され、現在、主要なモダンブラウザ(Chrome, Firefox, Safari, Edge)では完全にサポートされています。

ただし、古い環境(IEなど)をサポートする必要がある場合は、Babelなどのトランスパイラを使用して、ES5互換のコードに変換する(Promiseを多用したGeneratorベースのコードに変換する)必要があります。この変換処理は通常、WebpackやRollupなどのビルドツールに組み込まれています。

try...catchで捕捉できない非同期エラーの注意点

awaitを伴わない非同期処理で発生したエラーは、外側のtry...catchでは捕捉できません。

JavaScript

async function execute() {
  try {
    // [1] awaitが付いているため、エラーはtry...catchで捕捉できる
    await fetch('/api/user-fail'); 

    // [2] awaitが付いていないため、エラーは捕捉できない(未処理のPromise Rejectionとなる)
    fetch('/api/user-fail-2'); 
  } catch (e) {
    console.error('キャッチできたエラー:', e); // [1]のエラーのみを捕捉
  }
}

awaitを伴わない非同期関数は、メインスレッドのtry...catchのスコープ外で実行されてしまうため、予期せぬエラーを防ぐためにも、Promiseを扱う際には必ずawaitするか、Promise自体に.catch()を付けてエラーを処理する必要があります。

最後に

Async/Awaitは、非同期処理の複雑さを解消し、可読性を大幅に向上させるJavaScriptの強力な機能です。

  • Promiseの進化形:
    Promiseベースの処理を、.then()の連鎖から解放し、上から下に流れる分かりやすいコードに変換します。
  • シンプルなエラーハンドリング:
    try...catch構文により、非同期処理のエラーを直感的に扱えます。
  • 並列処理の深化:
    Promise.allSettled()を活用することで、エラー耐性の高い堅牢な非同期並列処理を実装できます。

Async/Awaitを使いこなすことで、あなたの非同期コードはよりクリーンで、メンテナンスしやすいものへと進化するでしょう。

以上、JavaScript Async/AwaitとPromiseの連携についてのご紹介でした!

この記事を書いた人

Ryohei

Webエンジニア / ブロガー

福岡のWeb制作会社に務めるWebエンジニアです。エンジニア歴は10年程で、好きな言語はPHPとJavaScriptです。本サイトは私がインプットしたWebに関する知識を整理し、共有することを目的に2015年から運営しています。Webに関するご相談があれば気軽にお問い合わせください。