JavaScriptの基礎 - async await
概要
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
戻り値のパターンは以下の通りである。
| 記述 | 変換結果 |
|---|---|
| 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ブロック |
|
下表に、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を使用する時の制約と注意事項を示す。
| 使用可否 | 環境 |
|---|---|
| 使用可能 |
|
| 使用不可 |
|
トップレベル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 を使用して並列実行することでパフォーマンスを向上できる。
関連情報