Node.jsはサーバーサイドJavaScriptの実行環境として、誕生以来圧倒的な支持を集め続けてきました。
2026年現在、Node.jsのランタイムはさらに洗練され、高度な非同期処理を効率的に記述することが開発者にとって不可欠なスキルとなっています。
かつて多くのエンジニアを悩ませた「コールバック地獄」は過去のものとなり、現代のJavaScript開発ではasync/awaitの活用が標準となりました。
この記事では、Node.jsにおける非同期処理の根幹を支えるasync/awaitの基礎から、最新のベストプラクティスまでを詳しく解説します。
非同期処理の仕組みを深く理解することで、読みやすく、かつパフォーマンスの高いアプリケーションを構築できるようになりましょう。
Node.jsにおける非同期処理の重要性
Node.jsの最大の特徴は、シングルスレッドでありながら高いスケーラビリティを実現する「ノンブロッキングI/O」にあります。
サーバーサイドでの処理において、データベースへのアクセスや外部APIの呼び出し、ファイルの読み書きには一定の時間がかかります。
もしこれらの処理を同期的に(順番に)実行してしまうと、処理が完了するまで次のリクエストを受け付けることができなくなります。
これを防ぐために、Node.jsでは「結果を待たずに次の処理を進める」非同期処理が採用されています。
かつてはコールバック関数を用いてこの非同期処理を制御していましたが、処理が複雑になるにつれてコードの可読性が著しく低下する問題がありました。
その後、Promiseが登場し、さらに構文糖衣としてasync/awaitが導入されたことで、非同期処理を同期処理のような直感的な記述で書くことが可能になりました。
async/awaitの基本構文
async/awaitを使いこなすための第一歩は、その基本的な書き方を正しく理解することです。
asyncキーワードの役割
関数を定義する際に、その前にasyncを記述することで、その関数は「非同期関数」となります。
async関数の最大の特徴は、戻り値が必ずPromiseオブジェクトになるという点です。
関数内で明示的にPromiseを返さず、単なる文字列や数値を返した場合でも、Node.jsはそれを自動的にResolved(解決済み)のPromiseでラップします。
async function greet() {
return "Hello, Node.js!";
}
const result = greet();
console.log(result); // Promise { 'Hello, Node.js!' }
Promise { 'Hello, Node.js!' }
awaitキーワードの役割
awaitは、Promiseが解決されるまで関数の実行を一時停止し、その結果を取り出すためのキーワードです。
重要なルールとして、awaitは原則としてasync関数の中でのみ使用可能です(トップレベルawaitなどの例外を除く)。
awaitを使用することで、Promiseの結果を待つコードを、あたかも上から下へ流れる同期的なコードのように記述できます。
async function fetchData() {
// 疑似的な非同期処理(Promise)
const data = await new Promise((resolve) => {
setTimeout(() => resolve("取得完了"), 1000);
});
console.log(data);
}
fetchData();
取得完了
実戦的なasync/awaitの使い方
基本的な構文を理解したところで、実際の開発現場で多用されるパターンを見ていきましょう。
標準モジュールでの活用
Node.jsの標準モジュールの多くは、Promiseを返すAPIを提供しています。
例えば、ファイル操作を行うfsモジュールでは、fs/promisesをインポートすることでasync/awaitを直接利用できます。
import { readFile } from 'node:fs/promises';
async function readConfigFile() {
try {
const content = await readFile('./config.json', 'utf-8');
const config = JSON.parse(content);
console.log("設定を読み込みました:", config);
} catch (error) {
console.error("読み込み失敗:", error.message);
}
}
readConfigFile();
エラーハンドリングの鉄則
非同期処理において、エラーハンドリングは最も重要な要素の一つです。
async/awaitでは、従来のJavaScriptと同様にtry…catch構文を使用してエラーを捕捉します。
Promiseの.catch()メソッドを使用するよりも、コードのネストが浅くなり、どこでエラーが発生したかを把握しやすくなります。
非同期処理における例外は、適切にキャッチしないとプロセスのクラッシュを引き起こす可能性があるため、必ず対策を講じる必要があります。
async function apiCall() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// ネットワークエラーやJSON解析エラーをここでキャッチ
console.error("API呼び出し中にエラーが発生:", error);
return null;
}
}
並行処理によるパフォーマンス最適化
async/awaitの初心者が陥りやすい罠として、「すべてを順番に待ってしまう」というものがあります。
複数の独立した非同期処理がある場合、一つずつawaitを重ねると、トータルの待ち時間はそれぞれの処理時間の合計になってしまいます。
これを回避し、複数の処理を並行して実行するには、Promise.allを活用します。
Promise.allによる並行実行
Promise.allは、複数のPromiseを配列として受け取り、それらがすべて完了するまで待機します。
この手法を用いることで、ネットワーク通信などの待機時間を大幅に短縮し、アプリケーションのレスポンスを向上させることができます。
async function getDashboardData() {
console.time("計測");
// 悪い例:順番に実行(直列実行)
// const user = await fetchUser();
// const posts = await fetchPosts();
// 良い例:同時に開始(並行実行)
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts()
]);
console.timeEnd("計測");
return { user, posts };
}
Promise.allSettledの使い分け
Promise.allは、渡されたPromiseのうち一つでもエラー(Reject)になると、全体がエラーとして扱われます。
しかし、「一部の処理が失敗しても、成功した分のデータだけは取得したい」というケースも少なくありません。
そのような場合には、Promise.allSettledを使用することで、個々の処理の結果(成功か失敗か)を詳細に把握できます。
const results = await Promise.allSettled([
fetchServiceA(),
fetchServiceB(),
fetchServiceC()
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`サービス${index}の結果:`, result.value);
} else {
console.error(`サービス${index}は失敗しました:`, result.reason);
}
});
Node.jsにおける最新の非同期イテレーション
配列などのコレクションに対して非同期処理を繰り返したい場合、通常のforEachメソッドでは意図した動作にならないことがあります。
forEachはPromiseの解決を待機しないため、ループ内のawaitが無視されてしまいます。
このようなケースでは、for await…of構文を使用するのが最適です。
async function processQueue(urls) {
for (const url of urls) {
// 順番に一つずつ処理を完了させてから次へ進む
const data = await fetch(url);
console.log(`処理完了: ${url}`);
}
}
また、ストリーム(Stream)などの非同期にデータが流れてくるオブジェクトに対しても、for await...ofは非常に強力な武器となります。
よくあるアンチパターンと回避策
async/awaitを使いこなすためには、避けるべき「悪い書き方」を知ることも重要です。
「Await地獄」を避ける
不必要にawaitを多用し、直列実行が重なることでパフォーマンスが低下する現象を「Await地獄」と呼びます。
前述のPromise.allを適切に使用し、依存関係のない処理は可能な限り並行化することを意識しましょう。
コールバック形式との混在
古いライブラリやAPIでは、まだコールバック関数を要求するものがあります。
そのような場合は、Node.js標準のutil.promisifyを使用して、コールバック形式をPromise形式に変換してからawaitで使用しましょう。
import { promisify } from 'node:util';
import { exec } from 'node:child_process';
const execPromise = promisify(exec);
async function runCommand() {
const { stdout, stderr } = await execPromise('ls -la');
console.log(stdout);
}
エラーの握りつぶし
空のcatchブロックを作成することは、デバッグを極めて困難にします。
エラーが発生した際は、必ずログに出力するか、上位の関数に再スローするようにしましょう。
Node.js 2026年時点での最新トレンド
現在のNode.js環境では、Top-level awaitが一般的になっています。
ESモジュール(.mjsファイル、またはpackage.jsonでtype: moduleを指定)環境では、関数の外側でも直接awaitを使用できます。
これにより、スクリプトの初期化処理などをより簡潔に記述できるようになりました。
// モジュール直下でawaitが可能
const dbConnection = await connectToDatabase();
console.log("データベースに接続しました");
export default dbConnection;
パフォーマンスとリソース管理
Node.jsでの非同期処理は強力ですが、リソースの管理を怠るとメモリリークやパフォーマンス劣化の原因となります。
AbortControllerによるキャンセル処理
長時間かかる非同期処理や、ユーザーの操作によって不要になったAPIリクエストは、適切にキャンセルすべきです。
Node.jsでは、AbortControllerを使用して、非同期処理を外部から中断する仕組みが提供されています。
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 5000); // 5秒でタイムアウト
try {
const response = await fetch('https://example.com/large-data', { signal });
const data = await response.json();
} catch (err) {
if (err.name === 'AbortError') {
console.log('リクエストがキャンセルされました');
}
}
イベントループのブロッキングを防ぐ
async/awaitを使用していても、CPU負荷の高い計算(巨大なJSONのパース、暗号化処理など)を同期的に行うと、イベントループが停止してしまいます。
非同期関数の中で重い計算を行う場合は、worker_threadsを使用するか、処理を細かく分割してsetImmediateなどでイベントループに制御を戻す工夫が必要です。
実装パターンの比較表
ここまで紹介した手法の使い分けを整理します。
| 手法 | 主な用途 | メリット |
|---|---|---|
| 単一のawait | 順序が重要な個別の非同期処理 | コードが読みやすい |
| Promise.all | 複数の独立した処理を同時に実行 | 実行時間の短縮(最長のものに依存) |
| Promise.allSettled | 一部が失敗しても良い複数の処理 | エラーによる中断を防げる |
| for await…of | ストリームや非同期反復処理 | 順次処理を安全に行える |
実戦例:API集約サーバーの実装
最後に、学んだ知識を統合した実用的な例を紹介します。
複数の外部サービスから情報を取得し、一つのレスポンスとしてまとめるAPIエンドポイントを想定します。
async function aggregateData(userId) {
try {
// ユーザー情報の取得(これがないと後の処理ができないので単体で待つ)
const user = await fetchUserProfile(userId);
// 関連する統計データと活動履歴(互いに依存しないので並行実行)
const [stats, history] = await Promise.all([
fetchUserStats(userId),
fetchUserHistory(userId)
]);
return {
status: "success",
data: { user, stats, history }
};
} catch (error) {
console.error(`データ集約に失敗しました (User: ${userId}):`, error);
throw new Error("情報の取得に失敗しました。後ほど再試行してください。");
}
}
まとめ
Node.jsにおけるasync/awaitは、単なる便利な機能ではなく、現代の開発における必須の基盤技術です。
async関数は常にPromiseを返すこと、awaitは解決を待機すること、という基本を忘れないでください。
その上で、Promise.allを用いた並行処理の最適化や、try...catchによる堅牢なエラーハンドリングを組み合わせることが重要です。
2026年のNode.js開発環境では、さらに高速で安全な非同期処理のパターンが求められています。
この記事で紹介したテクニックを駆使して、効率的でメンテナンス性の高いJavaScriptコードを執筆していきましょう。
非同期処理を制する者は、Node.jsを制すると言っても過言ではありません。
