Node.jsにおけるアプリケーション開発では、特定の時間だけ処理を一時停止させる「スリープ」機能が必要になる場面が多々あります。
例えば、外部APIへのリクエスト制限を回避するためのレートリミット対策や、再試行処理(リトライ)の合間の待機時間の設定などが挙げられます。
しかし、Node.jsには他の言語で見られるような同期的な sleep() 関数が標準では組み込まれていません。
これは、Node.jsがシングルスレッドで動作するイベントループモデルを採用しており、スレッドを完全に止めてしまうことがアプリケーション全体の停止を意味するためです。
2026年現在のモダンな開発環境では、Promiseや async/await を活用した非同期的なスリープ実装が標準となっています。
本記事では、Node.jsで安全かつ効率的にスリープ処理を実装するための最新手法について、基礎から応用まで詳しく解説します。
なぜNode.jsには標準のsleep関数が存在しないのか
Node.jsの最大の特徴は、「ノンブロッキングI/O」という仕組みに基づいた設計にあります。
もしC言語やPythonのような同期的な sleep() をメインスレッドで実行すると、その間すべてのユーザーリクエストやタイマー処理が完全に止まってしまいます。
このようなブロッキング処理は、Webサーバーのレスポンス性能を著しく低下させるため、Node.jsの設計思想とは相反するものです。
そのため、Node.jsでは古くから setTimeout というコールバック形式の関数を用いて、指定時間後に処理を再開させる手法が取られてきました。
しかし、コールバック関数による実装は、処理が複雑になると「コールバック地獄」と呼ばれる可読性の低いコードを生む原因となります。
近年のNode.jsでは、これらの問題を解決するために、Promiseベースの非同期待機が推奨されています。
イベントループを止めずに「実行権を一度手放し、指定時間後に戻ってくる」という考え方が、Node.jsにおけるスリープの正体です。
従来のsetTimeoutによる待機処理
最も基本的な待機方法は、ブラウザのJavaScriptでもおなじみの setTimeout 関数を使用することです。
この方法は、指定したミリ秒が経過した後に、第1引数に渡した関数を実行するというものです。
// 従来のコールバック方式
console.log("処理を開始します");
setTimeout(() => {
console.log("2秒経過しました");
}, 2000);
console.log("この行はすぐに実行されます");
処理を開始します
この行はすぐに実行されます
2秒経過しました
実行結果から分かる通り、setTimeout 自体は非同期関数であるため、後続の処理が先に実行されてしまいます。
特定の処理を順番に実行したい場合、すべてのコードを setTimeout の中に記述しなければならず、コードの階層が深くなる欠点があります。
この課題を解決するために登場したのが、次に解説するPromiseを用いた実装です。
Promiseを活用したカスタムsleep関数の実装
モダンなJavaScriptでは、Promise オブジェクトと async/await 構文を組み合わせることで、同期的なコードに近い見た目でスリープを記述できます。
以下は、Promiseを自作してスリープを実現する一般的な手法です。
// Promiseを利用したsleep関数の定義
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function main() {
console.log("1秒待機します...");
// awaitを使って待機
await sleep(1000);
console.log("待機が完了しました");
}
main();
この実装のポイントは、Promiseの状態がresolve(解決)されるまで await が処理を一時停止させる点にあります。
await sleep(1000) と記述することで、メインスレッドをブロックすることなく、その関数内の実行順序のみを制御できます。
この手法は汎用性が高く、Node.jsのバージョンを問わず広く利用されてきました。
最新の推奨手法:node:timers/promisesモジュールの活用
現在のNode.jsにおいて最も推奨されるスリープの実装方法は、標準ライブラリの node:timers/promises を使用することです。
Node.js v15以降で導入されたこのモジュールを使用すれば、自分でPromiseをラップした関数を作成する必要がありません。
標準ライブラリとして提供されているため、信頼性が高く、コードの簡略化にもつながります。
基本的な使い方
node:timers/promises から setTimeout をインポートして使用します。
これは従来のグローバルな setTimeout とは異なり、最初からPromiseを返すように設計されています。
// ESM(ECMAScript Modules)での記述
import { setTimeout } from 'node:timers/promises';
async function execute() {
console.log("3秒間のスリープを開始します");
// 直接awaitで待機可能
await setTimeout(3000);
console.log("3秒経過しました");
}
execute();
モジュールをインポートするだけで、非常に直感的で美しいコードが書けるようになります。
2026年のプロジェクトにおいては、自作のsleep関数を作成するよりも、この標準モジュールを使用するのがベストプラクティスと言えます。
AbortSignalによるスリープの解除
node:timers/promises の setTimeout が優れている点は、「途中でスリープをキャンセルできる」という点にあります。
これは標準の AbortController APIと組み合わせることで実現可能です。
import { setTimeout } from 'node:timers/promises';
const ac = new AbortController();
const signal = ac.signal;
async function task() {
try {
console.log("長時間スリープ(10秒)を開始します...");
// signalを渡すことで、外部からキャンセル可能になる
await setTimeout(10000, undefined, { signal });
console.log("スリープが完了しました");
} catch (err) {
if (err.name === 'AbortError') {
console.log("スリープが途中でキャンセルされました!");
} else {
throw err;
}
}
}
task();
// 2秒後にキャンセルを実行
global.setTimeout(() => {
ac.abort();
}, 2000);
長時間スリープ(10秒)を開始します...
スリープが途中でキャンセルされました!
この仕組みを利用すれば、ユーザーの操作やタイムアウト監視によって、不要になった待機処理を即座に終了させることができます。
これは、大規模なアプリケーションにおいてリソースを効率的に管理するために非常に重要なテクニックです。
各実装手法の比較
Node.jsで利用可能なスリープ手法の特徴を以下の表にまとめました。
| 手法 | 推奨度 | 特徴 | 主な用途 |
|---|---|---|---|
| setTimeout (Callback) | 低 | 非推奨ではないがコードが煩雑になる | 単純な遅延実行 |
| Promise + setTimeout | 中 | 環境を選ばず動作する。定番の手法 | 共通ライブラリの作成など |
| node:timers/promises | 高 | 標準機能で最も洗練されている。キャンセル可能 | モダンなNode.js開発全般 |
| Atomics.wait (WorkerThreads) | 低 | 完全同期だがメインスレッドをフリーズさせる | 特殊な並列計算処理のみ |
基本的には node:timers/promises を選択すれば間違いありません。
実践的なユースケース:リトライ処理の実装
スリープが最も活躍するのは、ネットワーク通信などでエラーが発生した際のリトライ(再試行)処理です。
一定時間の間隔(バックオフ)を空けずにリトライを繰り返すと、相手側のサーバーに過度な負荷をかけてしまいます。
import { setTimeout } from 'node:timers/promises';
async function fetchDataWithRetry(url, retries = 3) {
for (let i = 1; i <= retries; i++) {
try {
console.log(`リクエスト試行 ${i}回目...`);
// 擬似的なフェッチ処理
if (Math.random() > 0.7) return "成功データ";
throw new Error("通信失敗");
} catch (err) {
if (i === retries) throw err;
const waitTime = i * 1000; // 徐々に待ち時間を長くする
console.log(`${waitTime}ms 待機して再試行します`);
await setTimeout(waitTime);
}
}
}
fetchDataWithRetry("https://example.com/api").then(console.log).catch(console.error);
このように、ループ処理の中で await setTimeout() を呼び出すことで、スマートなリトライアルゴリズムを簡単に実装できます。
実装時の注意点とパフォーマンス
スリープ処理を実装する際には、いくつか注意すべき点があります。
まず、「非常に短いスリープ(1msなど)をループ内で大量に実行しない」ことが重要です。
Node.jsのタイマー精度はOSのスケジューラに依存するため、ミリ秒単位の厳密な精度は保証されません。
また、数千、数万もの非同期スリープを同時に走らせると、メモリ消費量が増大し、イベントループの負荷が高まる可能性があります。
さらに、Atomics.wait を使った完全同期的なスリープという手法も存在しますが、これはメインスレッドで使用するとUIやサーバーの応答が完全に止まるため、使用は避けてください。
あくまで「非同期的な待機」を行うことが、Node.jsにおける正しいスリープのあり方です。
まとめ
Node.jsにおけるスリープの実装は、言語の進化とともに大きく変化してきました。
2026年現在の開発においては、従来の setTimeout コールバックを直接使うのではなく、Promiseベースの処理が標準となっています。
特に、Node.jsの標準モジュールである node:timers/promises を利用した await setTimeout() の実装は、最も可読性が高く、かつ高機能です。
AbortControllerによるキャンセル処理も可能なこの手法は、堅牢なアプリケーション開発に欠かせない要素です。
今回紹介した手法を適切に使い分け、メインスレッドをブロックしないクリーンな非同期コードを記述しましょう。
正しいスリープの実装をマスターすることで、エラーに強く、メンテナンス性の高いNode.jsアプリケーションを実現できるはずです。
