MENU

【JavaScript】非同期処理の基本を解説|Webの基礎

JavaScriptでの非同期処理(Promise・async/await)の基本の解説|Webの基礎
目次

はじめに

前回の記事では、複数の値をまとめて扱えるオブジェクトの基本を解説しました。

今回からは応用編として、非同期処理の基本を解説します。非同期処理は、サーバーとの通信や時間のかかる処理を扱うために欠かせない仕組みで、次回解説する「Fetch APIによるAjax通信」の土台になる知識です。VBAのマクロは基本的に上から順番に処理が進みますが、JavaScriptでは「時間のかかる処理を待たずに、先に他の処理を進める」という考え方が出てきます。まずはこの考え方と、それを扱うためのPromise・async/awaitという書き方を押さえていきましょう。

同期処理と非同期処理の違い

まず、次のコードを見てください。

console.log("1. 開始");

setTimeout(() => {
    console.log("2. 3秒後の処理");
}, 3000);

console.log("3. setTimeoutの直後");

上から順番に実行されるなら「1 → 2 → 3」の順に表示されそうですが、実際の実行結果は次のようになります。

1. 開始
3. setTimeoutの直後
2. 3秒後の処理

setTimeoutは「指定した時間が経過したら、渡した関数を実行する」という命令です。JavaScriptはこの3秒を待たずに次の行(console.log("3. ..."))へ進んでしまうため、3秒後の処理は一番最後に実行されます。このように「今の処理の完了を待たずに、次の処理を先に進める」働き方を非同期処理、逆に「上から順番に、1つずつ処理が終わるのを待ってから次に進む」働き方を同期処理と呼びます。

サーバーへの通信やファイルの読み込みのように「実際にどれくらい時間がかかるか分からない処理」を同期処理のまま書いてしまうと、その処理が終わるまで画面が固まったように見えてしまいます。JavaScriptでは通信のような処理の多くが非同期処理として作られており、次回解説するFetch APIも非同期処理の仕組みの上に成り立っています。

同期処理では上から順番に実行されるのに対し、非同期処理では時間のかかる処理を待たずに後続の処理が先に実行される様子を示すタイムライン図

コールバック関数による非同期処理(おさらいと問題点)

関数の使い方を解説した記事で触れたとおり、JavaScriptでは関数を値として他の関数に渡すことができます。先ほどのsetTimeoutの第1引数のように、非同期処理が終わったタイミングで実行してほしい関数を渡す使い方を、特にコールバック関数と呼びます。

function getUser(id, callback) {
    setTimeout(() => {
        callback({ id: id, name: "山田太郎" });
    }, 1000);
}

getUser(1, (user) => {
    console.log(user.name); // 1秒後に"山田太郎"
});

この方法自体は問題ありませんが、「Aの結果を使ってBを行い、Bの結果を使ってCを行う」のように非同期処理を連続して行いたい場合、コールバック関数の中にさらにコールバック関数を書くことになり、どんどんネストが深くなってしまいます。

getUser(1, (user) => {
    getOrders(user.id, (orders) => {
        getOrderDetail(orders[0].id, (detail) => {
            console.log(detail);
            // さらに続く処理があると、どんどん右に伸びていく…
        });
    });
});

この状態はコールバック地獄と呼ばれ、処理の見通しが悪くエラー処理も書きにくくなります。この問題を解決するために生まれたのが、次に紹介するPromiseです。

Promiseの基本

Promiseは、「非同期処理が将来成功するか失敗するかを表す箱」のようなオブジェクトです。new Promise()の中で、成功したときに呼ぶresolveと、失敗したときに呼ぶrejectを使って結果を表します。

function wait(seconds) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`${seconds}秒待ちました`);
        }, seconds * 1000);
    });
}

wait(2)
    .then((result) => {
        console.log(result); // "2秒待ちました"
    })
    .catch((error) => {
        console.error(error);
    });

wait関数はPromiseを返す関数として作られており、呼び出す側は.then()で成功時の処理、.catch()で失敗時の処理をつなげて書けます。コールバック関数をネストさせる代わりに.then()を鎖のようにつなげられるため、これをPromiseチェーンと呼びます。

getUserPromise(1)
    .then((user) => getOrdersPromise(user.id))
    .then((orders) => getOrderDetailPromise(orders[0].id))
    .then((detail) => console.log(detail))
    .catch((error) => console.error(error));

先ほどのコールバック地獄と同じ処理でも、ネストが深くならずに横並びで書けるようになりました。

async/awaitで読みやすく書く

Promiseのチェーンはコールバック地獄よりは読みやすくなりますが、.then()が増えるとまだ追いにくい面があります。そこで使われるのがasync/awaitという書き方です。asyncを付けた関数の中では、awaitを使ってPromiseの結果を同期処理のように上から順番に書くことができます。

async function main() {
    const result = await wait(2);
    console.log(result); // "2秒待ちました"
}

main();

awaitは「このPromiseの結果が出るまで、この関数の中だけ処理を一時停止する」という命令です。画面全体が固まるわけではなく、main関数の中の処理だけが一時停止するイメージです。先ほどのPromiseチェーンをasync/awaitで書き直すと、次のようになります。

async function main() {
    const user = await getUserPromise(1);
    const orders = await getOrdersPromise(user.id);
    const detail = await getOrderDetailPromise(orders[0].id);
    console.log(detail);
}

.then()を使わずに、まるで同期処理のように上から順番に書けるようになりました。

awaitは、原則としてasyncを付けた関数の中でしか使えません。またasyncを付けた関数は、内部で何をreturnしても常にPromiseを返す関数になります。return "OK"と書いても、呼び出し側から見るとPromiseの中に"OK"が入って返ってくる形になります。

エラー処理(try/catch)

async/awaitでのエラー処理は、通常のJavaScriptと同じtry/catchをそのまま使えます。awaitしているPromiseが失敗(reject)すると、その場でcatchブロックに処理が移ります。

async function main() {
    try {
        const result = await wait(2);
        console.log(result);
    } catch (error) {
        console.error("エラーが発生しました:", error);
    } finally {
        console.log("処理が終了しました");
    }
}

finallyブロックは、成功・失敗のどちらであっても必ず実行したい処理(ローディング表示を消すなど)を書く場所として使います。

実務サンプル(注文処理のローディング表示)

これまでの内容を使って、注文ボタンを押してから結果が返ってくるまでの流れを再現してみます。サーバーとの通信は次回解説するため、ここではsetTimeoutで通信の遅延を再現した擬似的な関数(submitOrder)を使います。

function submitOrder(orderData) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (orderData.quantity > 0) {
                resolve({ status: "success", orderId: 12345 });
            } else {
                reject(new Error("数量は1以上で指定してください"));
            }
        }, 1500);
    });
}

async function handleSubmit(orderData) {
    showLoading(true);

    try {
        const result = await submitOrder(orderData);
        showResult(`注文が完了しました(注文番号:${result.orderId})`);
    } catch (error) {
        showResult(`エラー:${error.message}`);
    } finally {
        showLoading(false);
    }
}

handleSubmit({ quantity: 2 });

showLoading・showResultは、それぞれ「処理中…」の表示と結果メッセージの表示を切り替える関数だとします。tryの中でawait submitOrder(orderData)が完了するまで「処理中…」の表示が続き、成功すれば注文番号入りのメッセージ、quantityが0以下でrejectされればエラーメッセージが表示され、どちらの場合もfinallyでローディング表示が消える、という流れです。

「処理中...」のローディング表示から、注文番号入りの完了メッセージに切り替わる様子を示すモックアップ]

よくある質問・エラー対処

Q. async関数の戻り値はどうなりますか?

→ asyncを付けた関数は、中で何をreturnしても常にPromiseを返します。return "OK"と書けば、呼び出し側はawaitまたは.then()で"OK"という値を受け取れます。


Q. awaitはasync関数の外でも使えますか?

→ 基本的にはasyncを付けた関数の中でしか使えません。await is only valid in async functionsのようなエラーが出た場合は、その処理をasync functionで囲むか、既存のasync関数の中に処理を移動してください。


Q. Promiseとasync/awaitはどちらを使うべきですか?

→ 基本的にはasync/awaitの方が上から順番に読めて分かりやすいため、まずはこちらを使うとよいでしょう。ただし複数の非同期処理を同時に走らせたい場合など、Promiseの仕組みを直接使った方が便利な場面もあります。次回解説するFetch APIと合わせて、実際の通信処理の中で使い分けを見ていきます。

おわりに(まとめ)

まとめ

非同期処理の考え方さえ押さえておけば、次回解説するFetch APIでのサーバー通信もスムーズに理解できます。

  • ✅ 非同期処理は、時間のかかる処理の完了を待たずに次の処理を先に進める仕組み
  • ✅ コールバック関数を連続して使うと、ネストが深くなる「コールバック地獄」が起きやすい
  • ✅ `Promise`は非同期処理の成功・失敗を表すオブジェクトで、`.then()`・`.catch()`でつなげて書ける
  • ✅ `async`/`await`を使うと、Promiseの結果を同期処理のように上から順番に書ける
  • ✅ エラー処理は通常のJavaScriptと同じ`try`/`catch`をそのまま使え、`finally`で後片付けの処理を書ける

次回はこの知識を使って、実際にサーバーとデータをやり取りするFetch APIを解説します。

次回は、Fetch APIを使ったサーバーとの通信(Ajax通信)について解説予定です。

この記事が、少しでも誰かのお役に立てれば幸いです。

関連記事

当サイトの記事で使用したVBAなどのサンプルをDLできます

この記事のサンプルはありません!

ダウンロードページへは下のカードをクリックすればジャンプできます。
よろしければご利用ください!


【CSS】テキスト関連プロパティについて基本解説|Webの基礎


よかったらシェアしてね!
  • URLをコピーしました!
目次