JavaScriptの基礎 - async await

提供: MochiuWiki : SUSE, EC, PCB

2026年2月20日 (金) 23:22時点におけるWiki (トーク | 投稿記録)による版 (ページの作成:「== 概要 == async / await は ES2017 (ES8) で導入された非同期処理の構文であり、Promiseをベースとして構築されている。<br> async関数は常にPromiseを返し、await式はPromiseの解決を待機してその値を取得する。<br> <br> async / await を使用することにより、非同期処理をまるで同期処理のように記述できる。<br> Promiseチェーン (.then / .catch) と比較して、コードの読みや…」)
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)

概要

async / await は ES2017 (ES8) で導入された非同期処理の構文であり、Promiseをベースとして構築されている。
async関数は常にPromiseを返し、await式はPromiseの解決を待機してその値を取得する。

async / await を使用することにより、非同期処理をまるで同期処理のように記述できる。
Promiseチェーン (.then / .catch) と比較して、コードの読みやすさと保守性が大幅に向上する。

エラーハンドリングには try / catch / finally構文を使用する。
これにより、同期処理と非同期処理のエラーを統一した方法で扱うことができる。

async / await は逐次処理と並列処理の両方に対応している。
複数の非同期処理を並列実行する場合は Promise.all と組み合わせることで効率的な処理が可能になる。

ES2022以降では、ESモジュールのトップレベルでもawaitを使用できる (トップレベルawait)。
また、非同期イテレータとの組み合わせにより for await...of 構文も利用可能である。

Tauriアプリケーション開発においては、RustバックエンドとのIPC通信に invoke() 関数を使用するが、この関数はPromiseを返すため、async / await との親和性が高い。

Tauri(Rust + React)では、フロントエンド(WebView)と バックエンド(Rust)間のプロセス間通信を Tauri IPC と呼ぶ。


async関数

async関数の宣言

async関数は async キーワードを付与することで定義する。
関数宣言、関数式、アロー関数、メソッド定義等、全ての関数定義形式に対応している。

 // 関数宣言
 async function fetchData() {
    const response = await fetch('/api/data');
    return response.json();
 }
 
 // 関数式
 const fetchData = async function() {
    const response = await fetch('/api/data');
    return response.json();
 };
 
 // アロー関数
 const fetchData = async () => {
    const response = await fetch('/api/data');
    return response.json();
 };
 
 // オブジェクトメソッド
 const obj = {
    async fetchData() {
       const response = await fetch('/api/data');
       return response.json();
    }
 };
 
 // クラスメソッド
 class DataService {
    async fetchData() {
       const response = await fetch('/api/data');
       return response.json();
    }
 }


async関数の戻り値

async関数の戻り値は、常にPromiseでラップされる。
この動作はJavaScriptエンジンによって自動的に処理される。

 async function example() {
    return 42;
 }

 // 上記は以下と同等
 function example() {
    return Promise.resolve(42);
 }

 example().then(value => console.log(value));  // 42


戻り値のパターンは以下の通りである。

async関数の戻り値の変換
記述 変換結果
return <値> Promise.resolve(値) に変換される
throw <エラー> Promise.reject(エラー) に変換される
return (値なし) Promise.resolve(undefined) に変換される


 async function resolved() {
    return 'success';
 }
 
 async function rejected() {
    throw new Error('failure');
 }
 
 async function empty() {
    return;
 }
 
 resolved().then(v => console.log(v));             // "success"
 rejected().catch(e => console.error(e.message));  // "failure"
 empty().then(v => console.log(v));                // undefined



await式

awaitの基本

await 式は async関数の内部でのみ使用可能である。(ES2022以降のトップレベルawaitを除く)
await の後にPromiseを記述することにより、そのPromiseが解決されるまで処理を一時停止し、解決値を取得する。

 async function example() {
    // Promiseが解決されるまで待機する
    const value = await somePromise;
    console.log(value);
 
    // 非Promiseの値にもawaitは使用できる (即座に値を返す)
    const number = await 42;
    console.log(number);  // 42
 }


await されたPromiseが拒否された場合、awaitの箇所で例外がスローされる。
この例外は、try / catchで捕捉できる。

 async function example() {
    try {
       const value = await Promise.reject(new Error('rejected'));
    }
    catch (error) {
       console.error(error.message);  // "rejected"
    }
 }


awaitとPromiseチェーンの比較

async / await は Promiseチェーンと同等の処理を、より読みやすい形式で記述するための構文糖衣 (Syntactic Sugar) である。
以下に同じ処理をPromiseチェーンとasync / awaitで記述した例を示す。

  • Promiseチェーン (Before)
     function fetchUserData(userId) {
        return fetch(`/users/${userId}`)
           .then(response => response.json())
           .then(user => {
              return fetch(`/posts/${user.id}`)
                 .then(response => response.json())
                 .then(posts => ({ user, posts }));
           })
           .catch(error => console.error(error));
     }
    

  • async / await (After)
     async function fetchUserData(userId) {
        try {
           const response = await fetch(`/users/${userId}`);
           const user = await response.json();
           const postsResponse = await fetch(`/posts/${user.id}`);
           const posts = await postsResponse.json();
           return { user, posts };
        }
        catch (error) {
           console.error(error);
        }
     }
    


async / await を使用することにより、ネストが解消されてソースコードのフローが上から下へ直線的に読めるようになる。
.then / .catch の詳細については、JavaScriptの基礎 - Promiseのページを参照すること。


エラーハンドリング

try / catch / finally

async関数内のエラーハンドリングには try / catch / finally 構文を使用する。
この構文は同期処理のエラーハンドリングと同じ記法であり、直感的に理解できる。

 async function processData(url) {
    try {
       const response = await fetch(url);
       if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
       }
       return await response.json();
    }
    catch (error) {
       console.error(error.message);
       return null;
    }
    finally {
       console.log('処理完了');  // 成功・失敗に関わらず実行される
    }
 }


try / catch / finally の各ブロックの役割を以下に示す。

try / catch / finally の各ブロックの役割
ブロック 説明
tryブロック
  • 正常な処理フローを記述する。
  • await式はこのブロック内に記述する。
catchブロック
  • エラーが発生した場合の処理を記述する。
  • awaitされたPromiseの拒否もここで捕捉される。
finallyブロック
  • 成功・失敗に関わらず必ず実行される処理を記述する。
  • リソースの解放やローディング状態の解除に使用する。


下表に、return awaitについての注意事項を示す。

try / catch ブロックの内部では return await を使用する必要がある。
await を省略すると、Promiseが拒否された場合にcatchブロックで捕捉できなくなるためである。

 // OK : try / catch内ではreturn awaitが必要
 async function withErrorHandling() {
    try {
       return await riskyOperation();  // awaitが必要
    }
    catch (error) {
       return null;
    }
 }
 
 // OK : try / catch無しの場合、awaitは不要 (余分なマイクロタスクを避けられる)
 async function simple() {
    return fetchData();  // awaitは不要
 }


複数のawaitでのエラー処理

複数のawait式が存在する場合、try / catch を1つにまとめると最初のエラーで後続の処理が中断される。
各awaitを個別のtry / catchで囲むことで、それぞれのエラーを独立して処理できる。

 async function processMultiple() {
    let user, posts;
 
    // ユーザ取得のエラーを個別に処理する
    try {
       user = await fetchUser();
    }
    catch (error) {
       console.error('ユーザー取得エラー:', error);
       user = null;
    }
 
    // 投稿取得のエラーを個別に処理する
    try {
       posts = await fetchPosts();
    }
    catch (error) {
       console.error('投稿取得エラー:', error);
       posts = [];
    }
 
    return { user, posts };
 }


この方法により、ユーザ取得に失敗しても投稿取得の処理を継続できる。
どちらのエラーも許容できない場合は、1つの try / catch にまとめてよい。


逐次処理と並列処理

逐次実行

await式を順番に記述すると、前のPromiseが解決されてから次のPromiseを開始する逐次実行となる。
各処理が前の処理の結果に依存する場合は、逐次実行が適切である。

 async function sequential() {
    // 各リクエストが完了してから次を開始する (合計: 2 + 3 + 1 = 6秒)
    const user = await fetch('/user').then(r => r.json());          // 2秒
    const posts = await fetch('/posts').then(r => r.json());        // 3秒
    const comments = await fetch('/comments').then(r => r.json());  // 1秒
 
    return { user, posts, comments };
 }


並列実行 (Promise.allとの組み合わせ)

互いに依存しない複数のPromiseは、Promise.all を使用して並列実行できる。
Promise.all は全てのPromiseが解決されたときに解決する新しいPromiseを返す。

 async function parallel() {
    // 全てのリクエストを同時に開始する (合計: max(2, 3, 1) = 3秒)
    const [user, posts, comments] = await Promise.all([
       fetch('/user').then(r => r.json()),
       fetch('/posts').then(r => r.json()),
       fetch('/comments').then(r => r.json())
    ]);
 
    return { user, posts, comments };
 }


並列実行では、最も時間のかかる処理が全体の待機時間を決定する。
上記の例では、6秒から3秒へと待機時間が半減する。

Promise.all はいずれかのPromiseが拒否された時点でただちに拒否される。
全ての結果が必要な場合は Promise.all を、一部の失敗を許容する場合は Promise.allSettled を使用する。

注意 : awaitの連続は直列化される

配列リテラル内でawaitを連続して記述すると、直列実行になってしまうため注意が必要である。

 // OK : 並列実行 (全て同時に開始する)
 const [r1, r2, r3] = await Promise.all([promise1, promise2, promise3]);
 
 // NG : 直列実行になる (promise1 -> promise2 -> promise3 の順に待機する)
 const results = [await promise1, await promise2, await promise3];


Promise.allに渡す配列を生成する時は、Promiseオブジェクト (またはPromiseを返す関数呼び出し) を先に評価してから await Promise.all() で待機する点に注意する。


トップレベルawait

トップレベルawaitとは

ES2022で導入されたトップレベルawaitは、ESモジュールのトップレベル (async関数の外側) でawaitを使用できる機能である。
これにより、モジュールの初期化処理に非同期処理を組み込むことができる。

 // config.mjs (ESモジュール)

 const config = await fetch('/config.json').then(r => r.json());
 
 export { config };


 // database.mjs

 const db = await initializeDatabase();
 
 export async function query(sql) {
    return db.execute(sql);
 }


使用上の注意点

下表に、トップレベルawaitを使用する時の制約と注意事項を示す。

top-level awaitの使用可否
使用可否 環境
使用可能
  • .mjsファイル
  • package.jsonファイル"type": "module" を指定した.jsファイル
  • type="module" 属性を付与した <script> タグ
使用不可
  • CommonJSモジュール (require() 形式)
  • type="module" のない通常の <script> タグ


トップレベルawaitを使用するモジュールをインポートする側は、そのモジュールのawaitが解決されるまでインポートが完了しない。
モジュールの依存関係が深い場合、初期化時間が長くなる可能性があるため注意すること。


asyncイテレーション

for await...of

for await...of 構文は、非同期イテラブルオブジェクトを順次処理するための構文である。
非同期ジェネレータ関数 (async function*) と組み合わせることにより、ストリーム処理やページネーション処理を直感的に記述できる。

 // 非同期ジェネレータ関数でページネーションを実装する
 async function* fetchPages(url) {
    let page = 1;
    while (true) {
       const response = await fetch(`${url}?page=${page}`);
       const data = await response.json();
 
       if (data.items.length === 0) break;
 
       for (const item of data.items) {
          yield item;
       }
       page++;
    }
 }
 
 // for await...ofで非同期イテレータを消費する
 async function processAllPosts() {
    for await (const item of fetchPages('/api/posts')) {
       console.log(item.title);
    }
 }


for await...of は非同期ジェネレータだけでなく、Symbol.asyncIterator を実装したオブジェクトにも使用できる。
これは、Node.jsのReadableStream等、データの逐次読み取りが必要な場面で活用できる。


Tauriでの使用例

Tauriアプリケーションでは、invoke() 関数を使用してRustバックエンドのコマンドを呼び出す。
invoke() はPromiseを返すため、async / await との組み合わせが自然である。

Reactコンポーネントでの基本的な使用例を以下に示す。

 import { useEffect, useState } from 'react';
 import { invoke } from '@tauri-apps/api/core';
 
 function UserList() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(false);
 
    useEffect(() => {
       // useEffect内でasync関数を定義して即時実行する
       const loadUsers = async () => {
          setLoading(true);
          try {
             const result = await invoke('get_users');
             setUsers(result);
          }
          catch (error) {
             console.error(error);
          }
          finally {
             setLoading(false);
          }
       };
       loadUsers();
    }, []);
 
    const handleDelete = async (id) => {
       try {
          await invoke('delete_user', { id });
          setUsers(prev => prev.filter(u => u.id !== id));
       }
       catch (error) {
          console.error(error);
       }
    };
 
    if (loading) return <div>読み込み中...</div>;
 
    return (
       <ul>
          {users.map(user => (
             <li key={user.id}>
                {user.name}
                <button onClick={() => handleDelete(user.id)}>削除</button>
             </li>
          ))}
       </ul>
    );
 }


useEffect内でasync関数を直接渡すことはできないため、内部でasync関数を定義して即時実行するパターンを使用する。

複数のTauriコマンドを並列実行する例を以下に示す。

 async function loadInitialData() {
    // 複数のTauriコマンドを並列実行する
    const [users, settings] = await Promise.all([
       invoke('get_users'),
       invoke('get_settings')
    ]);
 
    return { users, settings };
 }


複数のTauriコマンドに依存関係がない場合は、Promise.all を使用して並列実行することでパフォーマンスを向上できる。


関連情報