【JavaScript】waitとは?非同期処理で待ち時間を設定する方法を初心者向けにわかりやすく解説
はじめに
JavaScriptでは、ほとんどの操作がノンブロッキングで実行されるため、処理を「待たせる」という感覚がつかみにくいかもしれません。
しかし、実際の開発では、時間をかけて動く処理やAPIの呼び出しなど、ある程度の待機が必要になる場面があります。
そこで本記事では、JavaScriptで「待機」を実現する手法について、初心者の皆さんでも理解しやすい形で解説していきます。
この記事を読むとわかること
- JavaScriptにおける「待ち」とは何か
- 同期処理と非同期処理の基本的な仕組み
- 具体的な待機手法 (setTimeout、Promise、async/awaitなど) の使い方
- よくある失敗例や注意点
- 実務でどのように待機を組み込むか
これらを順番に見ていくことで、JavaScriptで待ち時間を扱う感覚をつかみましょう。
JavaScriptにおける「待ち」とは何か
JavaScriptではメインスレッド上で処理が行われ、基本的には次々と命令が実行されていきます。
他のプログラミング言語で一般的な「関数を呼び出して、その結果が返るまで停止して待機する」という動きは、標準的な書き方をするだけでは実現しにくいです。
その理由は、JavaScriptがシングルスレッドかつ非同期イベント駆動を基本とするためです。
一度に実行されるコードは1つだけで、他の処理を待つ間にブロックすることなく、別の処理が先に進む仕組みになっています。
一方で、ある処理が完了するのを待ってから次の処理を行いたい場面も多いです。
例えば、APIを呼び出してデータが返ってきてから画面表示をしたい場合などが挙げられます。
こうしたケースで効果的なのが、非同期処理の待機を適切に実装することなのです。
処理の流れを理解する
同期処理と非同期処理
JavaScriptのコードを読むときに混乱しがちなのが、同期処理と非同期処理の使い分けです。
同期処理は、一連の命令が上から順番に実行され、前の命令が終わるまで次の命令を待つイメージがあります。
例えば、配列の要素を順番に処理して合計を出すなどの単純なケースでは、同期的に進みます。
一方、非同期処理は、何かきっかけとなるイベント(タイマーやAPIレスポンスなど)が発生したタイミングで、別のコールバック関数やハンドラーが呼び出されます。
メインの処理は待たずに先へ進み、イベントが起きたときだけ関連する処理が行われます。
そのため、ふとした拍子に「思ったタイミングで値がセットされていない」という状況が発生しやすいです。
イベントループの仕組み
非同期の土台となるのが、イベントループと呼ばれる仕組みです。
JavaScriptランタイムは、キューに入ってくるイベントを1つずつ取り出して実行していきます。
setTimeoutやsetIntervalで指定した時間が来ると、その処理がキューに積まれ、順番に実行される形になります。
このイベントループを理解しておくと、待機を指定したはずのコードが思ったよりも早く(または遅く)実行される理由が見えてきます。
「なぜ一定時間待ったはずなのに、まだ処理が終わっていないのか」という疑問を持ったときにも、まずはイベントループの仕組みを頭に入れておくと混乱が少ないでしょう。
待機を実現するための手法
JavaScriptで処理を待たせる方法はいくつかあります。
典型的な方法としては、setTimeout、Promise、そしてasync/awaitが挙げられます。
それぞれの特徴と、実務でどう使い分けるかを見ていきましょう。
setTimeoutを使った待機
setTimeoutの基本的な使い方
JavaScriptで待機を考えると、まず思いつくのが setTimeout
です。
これは「指定したミリ秒後に関数を実行する」関数で、タイマーのように利用できます。
console.log("処理スタート"); // 2秒後に実行される setTimeout(() => { console.log("2秒が経過しました"); }, 2000); console.log("処理エンド");
このコードを実行すると、メインスレッドは setTimeout
の登録を済ませた後、すぐに次の命令(“処理エンド”の出力)へ進みます。
そして指定時間が経過した時点で、登録していた関数が呼び出されるという流れです。
実務シーンでの活用例
実務では、フォーム送信後に少しだけ待ってから画面遷移する、などのUI操作で用いることが考えられます。
ただし、本当に処理完了を待つ必要があるときは、タイマーによる待ちではなく、完了コールバックを活用するほうが確実です。
setTimeout
はあくまで時間ベースの待機なので、ネットワーク状況などによってはタイミングがずれたり、想定通りに動かない可能性があります。
setIntervalとの比較
setInterval
は指定した間隔で繰り返し処理を実行する関数です。
一見すると、繰り返し処理の中で少し待ってから何かをする場合には便利そうに見えます。
しかし、繰り返し動かす必要のない場面で setInterval
を使うと、逆に管理が複雑になります。
待機目的であれば setTimeout
を適宜呼び出すほうが扱いやすいといえるでしょう。
Promiseを使った待機
Promiseの基本
より柔軟な待機を実現する方法として、Promise があります。
Promiseは「非同期処理が完了したら値を返す」というオブジェクトで、完了(resolve)もしくは失敗(reject)すると、登録したコールバックが呼び出されます。
function waitForSomething() { return new Promise((resolve, reject) => { // ここで何らかの非同期処理を行う // 成功の場合 resolve("完了しました"); // 失敗の場合 // reject("エラーが発生しました"); }); } waitForSomething() .then((result) => { console.log(result); }) .catch((error) => { console.error(error); });
上記では waitForSomething()
内で実行される非同期処理が完了するのを待ってから、then
に定義した関数が呼び出されます。
待機を実現するという点でも、時間ベースではなく、「処理が本当に終わったかどうか」で制御できるため、実務的には使いやすいです。
Promiseを使うときの実務シーン
APIからのデータ取得は、とてもよくあるシーンです。
API呼び出しが終わるまで待って、その結果を画面に反映する、という流れならPromiseが適切です。
時間の経過ではなく、実際にレスポンスが返ってくるまで待機できるため、ユーザーの操作感としては正確に処理が完了してから次に進む形になります。
async/awaitを使った待機
async/awaitの基礎
Promiseをもっと直感的に扱えるようにした構文が、async/await です。
async
関数の中で await
を使うと、Promiseを返す処理が完了するまでそこで一時停止し、結果が返ってきたらその後を続けます。
async function fetchData() { const response = await fetch("https://example.com/api/data"); const jsonData = await response.json(); return jsonData; } async function main() { console.log("データ取得開始"); const data = await fetchData(); console.log("データ取得完了:", data); } main();
この書き方によって、あたかも同期処理のように「次の行を待つ」ことができます。
await
によってPromiseのresolveを待っている間も、ブロックされるのはその関数の実行だけです。
JavaScript全体が止まるわけではなく、イベントループは他の処理を平行して進められます。
実務シーンでのasync/await活用例
複数のAPIを呼び出す場合に、1つ目の結果を使って2つ目のAPIを呼び出すような場面があります。
そういったときに async/await
は、コードを見通しよく書く助けになります。
then
をチェーンさせるより、可読性が高くなることが多いでしょう。
また、エラーハンドリングも try/catch
構文が使いやすいです。
例外が発生した時点で、catch
ブロックに処理を移せるので、Promiseよりも自然に書けることが多いです。
実践例:複数のAPIを順番に呼び出して待機
非同期処理で待ち時間を設定する典型的なシーンとして、いくつかのAPIを順番に呼び出すというケースが挙げられます。
例えば、ユーザー情報を取得してから、そのユーザーに紐づく詳細データを呼び出すようなケースです。
例:データ取得後の画面更新
以下のように、1つのAPIから得た情報をもとに次のAPIを呼び出す場合、await
で順次処理が完了するのを待つ形がシンプルです。
async function getUserData(userId) { const userResponse = await fetch(`https://example.com/api/users/${userId}`); const user = await userResponse.json(); const detailResponse = await fetch(`https://example.com/api/users/${userId}/detail`); const detail = await detailResponse.json(); // 両方のデータを組み合わせて返す return { user, detail }; } async function main() { try { const { user, detail } = await getUserData(123); console.log("ユーザー:", user); console.log("詳細:", detail); } catch (error) { console.error("ユーザー情報の取得に失敗しました:", error); } } main();
このように、1つ目の fetch
が完了するのを待ってから次のAPIを呼ぶため、取得順が崩れません。
例:エラーが発生した際の待機
実務ではAPIが失敗することもあります。
その際、再試行するために少し待ってからもう一度APIを呼ぶといった戦略が取られることもあります。
そのような場合、setTimeout
と await
を組み合わせたり、待機用のPromiseを作って resolve
までの時間を稼いだりするなどの工夫が必要です。
function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function retryFetch(url, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP Error: ${response.status}`); } return await response.json(); } catch (error) { console.error("フェッチ失敗:", error); if (i < maxRetries - 1) { // 次のリトライまで1秒待機する例 await wait(1000); } else { throw error; } } } } async function main() { try { const data = await retryFetch("https://example.com/api/test"); console.log("最終結果:", data); } catch (error) { console.error("リトライを含むフェッチが失敗しました:", error); } } main();
ここでは wait
関数を用意して、一時的に待機できるPromiseを作っています。
エラーが発生したら、少し待ってから再度APIを呼ぶという流れを、シンプルに書くことができるでしょう。
UIをブロックしないためのコツ
JavaScriptの待機で大事なのは、ユーザーの画面操作をブロックしないことです。
たとえば巨大な計算を同期的に回してしまうと、その間はユーザーがボタンを押しても反応が止まったように見えてしまいます。
非同期処理を使うことで、メインスレッドをなるべく空けておくようにすると、画面表示が止まるようなストレスを与えずに済みます。
「待っている間にUIが固まらない」というのは、大切な使いやすさのポイントといえるでしょう。
ブロッキングのリスクを避けるためにも、時間のかかる処理は非同期化するなどの工夫をすることが重要です。
注意したいポイントとよくある失敗例
長いループと待機の問題
大きな配列をループ処理しながら一時停止したいケースで、同期的にループしてしまうと、数千件・数万件の処理が終わるまで画面がフリーズするかもしれません。
非同期的に少しずつ処理を行うか、Web Workerなど別スレッドで計算を行う方法を検討したほうがいい場合もあります。
ネットワーク遅延への対応
タイマーで指定した2秒や3秒が過ぎたからといって、必ずしもデータが返ってくるわけではありません。
ネットワークの状態次第で大きく遅延する可能性もあります。
時間ベースではなく、Promiseやasync/awaitで完了を待つことを優先すると、厳密な制御が行いやすくなるでしょう。
コールバックとの使い分け
歴史的には、コールバック関数を用いて複数の非同期処理を制御する書き方が一般的でした。
しかし、コールバックの入れ子が深くなると、コードの可読性が低下し、いわゆる「コールバック地獄」が発生します。
Promiseやasync/awaitを使うと、フラットなコードが書きやすくなるので、読みやすさが格段に向上することがあります。
ただし、既存のライブラリがコールバックを前提としているなどの場面では、コールバックを使わざるを得ないこともあるでしょう。
その場合は、Promise化するユーティリティ関数を用意してラップするなどの工夫もありえます。
スリープ関数のような疑似的待機
スリープ関数の実装例
時々「JavaScriptでスリープ(処理を強制的に停止)したい」という要望があります。
先ほどの例にもあったように、wait(ms)
のようにPromiseを返す関数を作れば、await
と組み合わせて実質的なスリープを実現できます。
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function example() { console.log("処理開始"); await sleep(2000); console.log("2秒後に処理再開"); } example();
ただし、これは「関数の中で待つ」手法であり、JavaScript全体をブロックするわけではありません。
実際の意味でスレッドを停止させることはできず、あくまで非同期制御の文脈で「ここで再開を遅らせる」だけです。
実務ではあまり使わない理由
一般的に、画面を使いやすく設計しようとすると、時間で無理に停止させるのは得策ではないことが多いです。
ネットワーク遅延やユーザー操作など、変数要素がある中で「とにかくn秒止めてみる」というのは、あまり安定した挙動をしません。
また、意図せずユーザーを待たせてしまい、操作感の悪化を招く場合もあります。
そのため、処理を同期的に止めたいのではなく、**「あるイベントが終わるのを待ちたい」**という状況なら、Promiseやasync/awaitを使うアプローチが主流になります。
非同期処理のテスト
非同期処理を行う関数は、テスト時にも待機を考慮する必要があります。
例えば、テストフレームワークでPromiseベースのAPIをテストするときは、テスト内で await
を使うか、done
コールバックを呼び出す仕組みになっていることが多いです。
思わぬタイミングでテストが終わってしまわないよう、テストの中でもきちんと非同期の待機を管理する必要があります。
テスト環境でも「どのように待つか」を意識することで、本番と同じような挙動を再現しやすくなります。
まとめ
JavaScriptの「待ち」は、同期処理のように関数がブロックされるわけではなく、非同期処理の完了を待つ形で表現されます。
この待機を実現するためには、setTimeout
、Promise
、async/await
といった仕組みをうまく組み合わせることがポイントです。
特に、複数のAPI呼び出しやネットワークを経由するような処理では、時間ベースよりも非同期処理の完了タイミングを基準にしたほうが、正確かつ使いやすいプログラムになります。
また、待っている間にUIがフリーズしてしまわないよう、メインスレッドをブロックしない書き方を心がけましょう。
ここまで見てきたように、JavaScriptでは「完全な停止」は起こりにくい分、開発者が意図的に待機を入れる工夫が必要です。
実際にはPromiseチェーンやasync/awaitを使うことでシンプルにコーディングできるので、初心者の皆さんもまずはPromiseとasync/awaitに慣れるところから始めてみるとよいでしょう。