JavaScriptのコールバック関数入門:基本からPromise、async/awaitまで徹底解説

目次

1. はじめに:コールバック関数とは何か?

プログラミングの世界、特にJavaScriptを扱う上で、「コールバック関数」という言葉を耳にする機会は多いでしょう。しかし、その正確な意味や目的、使い方について、曖昧な理解のままになっている方もいるかもしれません。この記事では、コールバック関数の基本的な定義から、その具体的な利用方法、そして現代のプログラミングにおける位置づけまで、初心者にもわかりやすく解説していきます。

コールバック関数の基本的な定義

コールバック関数とは、非常にシンプルに言うと、他の関数に引数として渡される関数のことです 1。関数を呼び出す際に、別の関数を「後で呼び出してね」という意味合いで渡します。そして、引数として関数を受け取った側の関数(高階関数とも呼ばれます)が、自身の処理中の適切なタイミングで、渡された関数を「呼び出し返す(Callback)」ことから、この名前が付けられました 1

MDN web docsでは、「コールバック関数とは、引数として他の関数に渡され、外側の関数の中で呼び出されて、何らかのルーチンやアクションを完了させる関数のこと」と定義されています 3

なぜこのようなことが可能なのでしょうか?それは、JavaScriptが関数を**「第一級オブジェクト(First-Class Citizen)」**として扱える言語だからです。つまり、数値や文字列といった他のデータ型と同じように、関数を変数に代入したり、他の関数の引数として渡したり、関数の戻り値として返したりすることができます 2。この言語仕様があるからこそ、処理の一部を関数として外部から注入し、柔軟なプログラムを構築するコールバックパターンが実現できるのです。

コールバック関数の目的と動機

では、なぜコールバック関数を使うのでしょうか?主な目的は以下の通りです。

  1. 柔軟性とカスタマイズ性: コールバック関数を使うと、ある関数の基本的な動作(例えば、配列の要素を順番に処理するなど)はそのままに、具体的な処理内容(各要素に対して何をするか)を、その関数を呼び出す側が自由に定義できます 5。これにより、関数を再利用しやすくなり、汎用性が高まります。例えば、配列の要素をチェックする関数で、チェック条件そのものをコールバック関数として渡せば、様々な条件に対応できる汎用的な関数になります 5。共通の骨組み(フレームワーク)を用意し、カスタマイズしたい部分だけをコールバックとして記述することで、コードの重複を減らすことができます 12
  2. 非同期処理の実現: JavaScriptにおけるコールバック関数の最も重要な役割の一つが、非同期処理のハンドリングです。非同期処理とは、完了までに時間がかかる可能性のある処理(例:サーバーからのデータ取得、タイマー処理、ファイルの読み書きなど)を待たずに、次の処理へ進む仕組みのことです 14。このような時間のかかる処理が終わったに実行したい処理をコールバック関数として登録しておくことで、プログラム全体の流れを止めずに(ノンブロッキング)、効率的に処理を進めることができます 11
  3. イベント処理: ユーザーインターフェース(UI)を持つアプリケーションでは、ユーザーのアクション(ボタンのクリック、キーボード入力など)に応じて特定の処理を実行する必要があります。このようなイベント駆動型のプログラミングにおいて、コールバック関数(この文脈ではイベントハンドラやイベントリスナーと呼ばれることが多い)は中心的な役割を果たします 10。特定のイベントが発生した時に実行されるべき処理を、コールバック関数として登録しておくのです。

簡単な例え話

コールバック関数の概念を、より身近な例で考えてみましょう。

  • レストランでの注文: レストランで食事を注文する際に、「食事が終わったら、デザートのメニューを持ってきてください」とウェイターにお願いする場面を想像してください 17。この「デザートのメニューを持ってくる」というアクションが、食事(=最初の処理)が終わった後に行われるべきコールバック処理に相当します。
  • お母さんへのお願い: お母さんがスーパーマーケットに行くときに、「ついでにお菓子も買ってきてね」とお願いするのも似ています 25。お母さんの「スーパーマーケットに行く」という処理の途中で、あなたの「お菓子を買う」という要求(関数)がコールバックとして実行されます。
  • 子供の宿題: 子供が「宿題が終わったらおやつちょうだい」と言うのも、コールバックの考え方に通じます 29。「宿題が終わる」というイベント(条件)に応じて、「おやつをあげる」というコールバックが実行されるわけです。

これらの例え話は、「特定のタイミングや処理の完了後に、あらかじめ指定しておいた別の処理を実行する」というコールバック関数の本質的な役割を示しています。

2. コールバック関数の具体的な使い方

コールバック関数の概念を理解したところで、次にJavaScriptでどのように記述し、利用するのかを具体的に見ていきましょう。

構文と呼び出し方

コールバック関数を利用するには、まずコールバック関数を受け取る側の関数(高階関数)を定義します。この関数は、引数の一つとして関数を受け取るように設計されます。

JavaScript

// コールバック関数を受け取る関数 ‘processData’
function processData(data, callback) {
  console.log(“処理を開始します…”);
  // 何らかのデータ処理を行う (ここでは単純化)
  const processedData = data.toUpperCase();
  // 処理が完了したら、コールバック関数を呼び出す
  console.log(“処理が完了しました。コールバックを実行します。”);
  callback(processedData); // 受け取った関数を呼び出す [2, 18]
}

次に、この processData 関数に渡すコールバック関数を用意します。コールバック関数は、事前に名前をつけて定義しておくことも、関数を呼び出す際に直接定義することも可能です。

1. 名前付き関数を渡す場合:

JavaScript

// コールバック関数として使用する関数 ‘displayData’
function displayData(result) {
  console.log(“結果を表示します:”, result);
}

// processData関数を呼び出し、displayDataをコールバックとして渡す
processData(“hello world”, displayData); [2, 18]

/*
出力:
処理を開始します…
処理が完了しました。コールバックを実行します。
結果を表示します: HELLO WORLD
*/

この例では、displayData という名前の関数を定義し、processData 関数の第二引数として渡しています。関数を渡す際には、関数名の後ろに () を付けないことに注意してください。() を付けると関数がその場で実行されてしまい、関数の定義そのものを渡すことになりません 2

2. 匿名関数(無名関数)を渡す場合:

コールバック関数がその場で一度しか使われない場合は、わざわざ名前を付けずに、関数を定義しながら直接引数として渡す「匿名関数(または無名関数)」がよく用いられます。特にアロー関数式 (=>) を使うと、より簡潔に記述できます 5

JavaScript

// processData関数を呼び出し、匿名関数をコールバックとして渡す
processData(“another example”, function(result) { // 従来の匿名関数
  console.log(“匿名関数で結果を表示:”, result);
});

// アロー関数を使った場合
processData(“yet another example”, (result) => { // アロー関数 [20]
  console.log(“アロー関数で結果を表示:”, result);
});

/*
出力:
処理を開始します…
処理が完了しました。コールバックを実行します。
匿名関数で結果を表示: ANOTHER EXAMPLE
処理を開始します…
処理が完了しました。コールバックを実行します。
アロー関数で結果を表示: YET ANOTHER EXAMPLE
*/

匿名関数は、特にイベントリスナーや一時的な処理を定義する際に便利です 5

同期コールバック vs 非同期コールバック

コールバック関数について学ぶ上で非常に重要な点は、すべてのコールバック関数が非同期的に実行されるわけではないということです 8。コールバック関数がいつ実行されるか(同期的か非同期か)は、そのコールバック関数を受け取る高階関数の実装によって決まります 30

  • 同期コールバック (Synchronous Callback):
    コールバック関数が、高階関数の処理の一部として即座に、かつ順番通りに実行される場合です。高階関数の実行は、コールバック関数の実行が完了するまでブロックされます。代表的な例は、JavaScriptの配列メソッド forEach や map です 8。
    JavaScript
    const numbers = [1, 2, 3];
    console.log(“forEachの実行前”);
    numbers.forEach(function(number) { // 同期コールバック
      // この関数は配列の各要素に対して即座に実行される
      console.log(“要素:”, number);
    });
    console.log(“forEachの実行後”);

    /*
    出力:
    forEachの実行前
    要素: 1
    要素: 2
    要素: 3
    forEachの実行後
    */

    この例では、「forEachの実行後」というログは、配列のすべての要素に対するコールバック関数の実行が完了したに表示されます。
  • 非同期コールバック (Asynchronous Callback):
    コールバック関数が、高階関数の実行完了を待たずに、後のある時点で実行される場合です。高階関数はコールバックの実行を待たずにすぐに処理を返し、プログラムの他の部分の実行をブロックしません。代表的な例は setTimeout やイベントリスナーです 8。
    JavaScript
    console.log(“setTimeoutの実行前”);
    setTimeout(function() { // 非同期コールバック
      // この関数は指定した遅延時間(1000ms)の後、非同期に実行される
      console.log(“setTimeout内のコールバック実行”);
    }, 1000);
    console.log(“setTimeoutの実行後”);

    /*
    出力:
    setTimeoutの実行前
    setTimeoutの実行後
    (約1秒後)
    setTimeout内のコールバック実行
    */

    この例では、「setTimeoutの実行後」というログは、setTimeout が呼び出された直後に表示されます。コールバック関数は、メインのコード実行フローから切り離され、指定された時間が経過した後に実行されます。

コールバック関数が非同期処理の文脈で紹介されることが多いため、「コールバック=非同期」と誤解されがちですが、この区別はJavaScriptの実行モデルを正確に理解する上で不可欠です 30。高階関数が同期的にコールバックを呼ぶのか、非同期的に呼ぶのかを意識することで、コードの実行順序を正しく予測し、意図しないバグを防ぐことができます。

イベント処理の例

非同期コールバックの典型的な例として、ブラウザ環境でのイベントリスナーを見てみましょう。

HTML

<button id=”myButton”>クリックしてください</button>

JavaScript

// JavaScriptファイル
const button = document.getElementById(‘myButton’);

console.log(‘イベントリスナーを追加します。’);

// ボタンがクリックされたときに実行されるコールバック関数を登録
button.addEventListener(‘click’, function() { // 非同期コールバック(イベントハンドラ) [10, 17, 22]
  console.log(‘ボタンがクリックされました!’);
});

console.log(‘イベントリスナーの追加が完了しました。クリックを待機中…’);

/*
出力:
イベントリスナーを追加します。
イベントリスナーの追加が完了しました。クリックを待機中…
(ボタンがクリックされると)
ボタンがクリックされました!
*/

この例では、addEventListener メソッドの第二引数に渡された関数がコールバック関数です。この関数は、addEventListener が呼び出された時点では実行されません。ブラウザが ‘click’ イベントを検知したときに、初めて非同期的に呼び出されます 10

3. 典型的な利用シナリオ

コールバック関数は、JavaScriptの様々な場面で活用されています。ここでは、特に代表的な利用シナリオをいくつか紹介します。

イベントリスナー / イベントハンドリング

前節でも触れましたが、ブラウザ上でのユーザーインタラクション(クリック、マウス移動、キー入力など)や、Node.jsにおけるストリームのイベント(データの受信、接続の完了など)を処理するために、コールバック関数は不可欠です 10

イベントが発生した際に実行したい処理ロジックをコールバック関数(イベントハンドラ)として定義し、addEventListener (ブラウザ) や .on() (Node.jsのEventEmitterなど) といったメソッドを使ってイベントに登録します。イベントが発生すると、JavaScriptの実行環境(ブラウザやNode.js)内のイベントループ機構が、登録されたコールバック関数を適切なタイミングで呼び出します 10

JavaScript

// ブラウザの例 (再掲)
button.addEventListener(‘click’, () => {
  console.log(‘クリックイベント発生!’);
});

// Node.jsのEventEmitterの例 (概念)
const EventEmitter = require(‘events’);
const myEmitter = new EventEmitter();

myEmitter.on(‘customEvent’, (arg1, arg2) => { // ‘customEvent’が発生したときのコールバック
  console.log(‘カスタムイベント発生! 引数:’, arg1, arg2);
});

// イベントを発火させる
myEmitter.emit(‘customEvent’, ‘データ1’, ‘データ2’);

タイマー処理

指定した時間の経過後に処理を実行したり、一定間隔で処理を繰り返したりする場合にも、コールバック関数が使われます。

  • setTimeout(callback, delay):
    指定した delay (ミリ秒単位) が経過した後に、callback 関数を一度だけ非同期的に実行します 8。簡単な遅延実行や、一度きりの非同期タスクのスケジュールに使用されます。
    JavaScript
    console.log(“タイマー開始”);
    setTimeout(() => {
      console.log(“3秒経過しました。”);
    }, 3000); // 3000ミリ秒 = 3秒
    console.log(“タイマー設定完了”);
  • setInterval(callback, interval):
    指定した interval (ミリ秒単位) ごとに、callback 関数を繰り返し非同期的に実行します 20。定期的なデータのポーリングやアニメーションの更新などに利用されます。clearInterval() 関数を使って、繰り返し処理を停止できます。
    JavaScript
    let count = 0;
    console.log(“インターバル開始”);
    const intervalId = setInterval(() => {
      count++;
      console.log(`インターバル実行 ${count}回目`);
      if (count >= 5) {
        clearInterval(intervalId); // 5回実行したら停止
        console.log(“インターバル停止”);
      }
    }, 1000); // 1000ミリ秒 = 1秒ごと
    console.log(“インターバル設定完了”);

Node.jsにおける非同期I/O

Node.jsは、「スケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動のJavaScript環境」とされています 34。その中心的な特徴は、ノンブロッキングI/O (非同期I/O) です 34

これは、ファイル読み書き、データベースアクセス、ネットワーク通信といったI/O(入出力)操作が、プログラム全体の実行を停止させないことを意味します。代わりに、Node.jsはI/O操作を開始した後、その完了を待たずに次の処理に進みます。そして、I/O操作が完了した時点で、結果(またはエラー)を処理するためのコールバック関数が呼び出されるのです 20

  • ファイルシステム (fs モジュール):
    Node.jsでファイルを非同期的に読み書きする際には、fs モジュールの関数(例: fs.readFile, fs.writeFile)がよく使われます。これらの関数の多くは、最後の引数としてコールバック関数を受け取ります。このコールバック関数は、慣例として最初の引数にエラーオブジェクト(エラーが発生しなかった場合は null や undefined)、二番目の引数に操作の結果(読み込んだデータなど)を受け取る「エラーファースト・コールバック (Error-first Callback)」という規約に従います 11。
    JavaScript
    const fs = require(‘fs’);

    console.log(‘ファイル読み込みを開始します…’);

    fs.readFile(‘example.txt’, ‘utf8’, (err, data) => { // エラーファースト・コールバック
      // I/O操作完了後にこのコールバックが実行される
      if (err) {
        // エラーが発生した場合
        console.error(‘ファイル読み込みエラー:’, err);
        return; // エラー処理をして終了
      }
      // 成功した場合
      console.log(‘ファイルの内容:’, data);
    });

    console.log(‘readFileの呼び出しが完了しました。結果を待機中…’);

    対照的に、fs.readFileSync のような同期版の関数も存在しますが、これらはI/O操作が完了するまでプログラム全体をブロックしてしまうため、特にサーバーサイドアプリケーションではパフォーマンス上の理由から非同期版の使用が推奨されます 39
  • HTTPリクエスト:
    他のサーバーやAPIと通信するためのHTTPリクエストも、Node.jsでは非同期に行われるのが一般的です。標準の http や https モジュール、あるいは axios や node-fetch といったライブラリを使用する際、レスポンスを受け取った後の処理をコールバック関数(または後述するPromise/async-await)で記述します 20。
  • 基盤となる仕組み (libuv):
    Node.jsがこれらの非同期I/O操作を効率的に処理できる背景には、libuv というライブラリの存在があります。libuv は、OSレベルの非同期機能(Linuxのepoll、macOS/BSDのkqueueなど)や、必要に応じてワーカースレッドプールを利用して、クロスプラットフォームで高性能な非同期I/Oを実現しています 41。これにより、Node.jsのメインスレッドはI/O待ちでブロックされることなく、他のリクエストの処理などに集中できるのです 39。

Node.jsの設計思想とコールバック関数は密接に結びついています。非同期I/Oモデルを実現するための基本的な仕組みとして、コールバック関数が初期のNode.jsにおいて中心的な役割を担っていました 21。現在では後述するPromiseやasync/awaitが主流となっていますが、Node.jsの非同期性の根幹を理解するためには、コールバック関数の役割を知ることが依然として重要です。

4. コールバック関数の課題:「コールバック地獄」

コールバック関数は非同期処理やイベント処理を実現する上で強力な仕組みですが、使い方によっては大きな課題も生じます。その最も有名なものが「コールバック地獄(Callback Hell)」または「破滅のピラミッド(Pyramid of Doom)」と呼ばれる問題です。

問題点

コールバック地獄は、複数の非同期処理を順番に実行する必要がある場合に発生しやすくなります。つまり、ある非同期処理Aが完了した後に、その結果を使って非同期処理Bを実行し、さらにBの結果を使って非同期処理Cを実行する…といった依存関係のある一連の処理を、コールバック関数だけで実装しようとすると、コールバック関数の中にさらにコールバック関数を記述する、というネスト(入れ子)構造が深くなっていくのです 22

以下は、コールバック地獄の構造を示す擬似コードです。

JavaScript

// コールバック地獄の例 (擬似コード)
asyncOperation1(input, (err1, result1) => {
  if (err1) {
    console.error(“エラー1:”, err1);
    return;
  }
  // 処理1が成功したら、その結果(result1)を使って処理2を開始
  asyncOperation2(result1, (err2, result2) => {
    if (err2) {
      console.error(“エラー2:”, err2);
      return;
    }
    // 処理2が成功したら、その結果(result2)を使って処理3を開始
    asyncOperation3(result2, (err3, result3) => {
      if (err3) {
        console.error(“エラー3:”, err3);
        return;
      }
      // 処理3が成功したら、さらに処理4へ…
      asyncOperation4(result3, (err4, result4) => {
        if (err4) {
          console.error(“エラー4:”, err4);
          return;
        }
        //… ネストはどこまでも深くなる可能性がある
        console.log(“最終結果:”, result4);
      });
    });
  });
});

このように、処理のステップが増えるにつれて、コードのインデントがどんどん深くなり、右側に突き出すような形(ピラミッド)になっていきます。

コールバック地獄がもたらす弊害

このようなコード構造は、いくつかの深刻な問題を引き起こします。

  • 可読性の低下: コードが深くまでネストされているため、処理全体の流れを把握するのが非常に困難になります 6。どこでどの処理が行われ、どの変数がどのスコープに属しているのかを追跡するのが難しく、コードを読むだけで疲弊してしまいます。
  • 保守性の低下: コードの修正やデバッグが非常に困難になります。新しい処理ステップを追加したり、既存のロジックを変更したりしようとすると、複雑なネスト構造の中で慎重に作業する必要があり、ミスを誘発しやすくなります。
  • エラーハンドリングの複雑化: 各非同期処理ステップでエラーが発生する可能性があるため、ネストの各レベルでエラーチェックとハンドリングを行う必要があります 20。これにより、エラー処理のコードが冗長になり、エラーの見逃しや、エラー情報の適切な伝播が難しくなることがあります。

コールバック地獄は、コールバック関数そのものが悪いというよりは、連続した非同期処理の制御フローと状態管理を、ネストされたコールバックという仕組みだけで行おうとすることの限界を示しています。非同期処理Aの結果に依存する非同期処理Bを実行するには、Bの呼び出しをAのコールバック内に配置するしかありません 22。この依存関係が連鎖すると、必然的にネストが深くなり、上記のような問題が発生します 6。この複雑さを解消するために、より優れた非同期処理のパターンとして、次に紹介するPromiseが登場しました。

5. コールバック関数の代替技術

コールバック地獄の問題を解決し、より構造化され、読みやすい非同期コードを書くために、JavaScriptには新しい技術が導入されました。それが Promiseasync/await です。

Promises

Promiseは、非同期処理の最終的な結果(成功または失敗)を表すオブジェクトです 16。コールバック関数を直接渡す代わりに、非同期処理を行う関数はPromiseオブジェクトを返します。このPromiseオブジェクトを通じて、非同期処理が完了したとき、または失敗したときの処理を登録することができます。

Promiseの状態:

Promiseオブジェクトは、以下の3つのいずれかの状態を持ちます 47:

  1. pending (待機中): 非同期処理がまだ完了していない初期状態。
  2. fulfilled (履行済み): 非同期処理が成功して完了した状態。結果の値を持つ。
  3. rejected (拒否済み): 非同期処理が失敗した状態。エラー(理由)を持つ。

Promiseは一度 pending から fulfilled または rejected に状態が変わると、それ以降状態が変わることはありません。

.then() メソッド:

Promiseの結果(成功した場合の値)やエラー(失敗した場合の理由)を処理するために、.then() メソッドを使用します 8。.then() は最大2つのコールバック関数を引数に取ります。

promise.then(onFulfilled, onRejected)

  • onFulfilled: Promiseが fulfilled になったときに実行されるコールバック。成功した結果の値が引数として渡されます。
  • onRejected: Promiseが rejected になったときに実行されるコールバック。失敗した理由(通常はエラーオブジェクト)が引数として渡されます。

.catch() メソッド:

エラー処理をより明確にするために、.catch() メソッドが用意されています 15。これは promise.then(null, onRejected) と同等で、Promiseが rejected になった場合のみ実行されるコールバックを登録します。

チェーン(連鎖):

Promiseの最も強力な特徴の一つが、**チェーン(連鎖)**です。.then() や .catch() メソッドは、新しいPromiseオブジェクトを返すため、複数の非同期処理をメソッドチェーンの形で繋げることができます 48。これにより、コールバック地獄のような深いネスト構造を避け、フラットで読みやすいコードを書くことが可能になります。

JavaScript

// Promiseを使った非同期処理の連鎖
asyncOperation1(input) // Promiseを返す関数
.then(result1 => {
    console.log(“処理1完了:”, result1);
    return asyncOperation2(result1); // 次の非同期処理(Promiseを返す)を呼び出し、そのPromiseを返す
  })
.then(result2 => {
    console.log(“処理2完了:”, result2);
    return asyncOperation3(result2); // さらに次の非同期処理へ
  })
.then(result3 => {
    console.log(“処理3完了:”, result3);
    console.log(“最終結果:”, result3);
  })
.catch(err => {
    // チェーンのいずれかのステップでエラーが発生した場合、このcatchブロックで捕捉される
    console.error(“エラー発生:”, err);
  });

このコードは、コールバック地獄の例と比べて、処理の流れが直線的で追いやすくなっています。また、エラーハンドリングも最後の .catch() で一元的に行うことができます。

Promise.all() など:

複数のPromiseを並行して実行し、すべてが完了するのを待つ Promise.all() や、複数のPromiseのうち最初に完了したものの結果を得る Promise.race() といったヘルパーメソッドも用意されており、より複雑な非同期フローを管理するのに役立ちます 48。

async/await

async/awaitは、Promiseをさらに扱いやすくするための構文糖衣(Syntactic Sugar)です 15。Promiseを基盤としていますが、非同期コードをあたかも同期コードのように記述できるようにすることで、可読性を劇的に向上させます 46

async キーワード:

関数の宣言の前に async キーワードを付けると、その関数は自動的にPromiseを返す非同期関数になります 46。関数内で return された値は、そのPromiseが fulfilled になったときの解決値となります。関数内でエラーがスローされると、返されるPromiseは rejected になります。

await 演算子:

await 演算子は、async 関数内でのみ使用できます 48。await の右側には通常Promiseオブジェクトを置きます。await は、そのPromiseが解決(fulfilled または rejected に)されるまで、async 関数の実行を一時停止します 43。

  • Promiseが fulfilled になると、await はその解決値を返します。
  • Promiseが rejected になると、await はその拒否理由(エラー)をスローします。

可読性とエラーハンドリング:

async/awaitを使うと、Promiseチェーンよりもさらに直線的で、同期処理に近い感覚で非同期処理を記述できます。エラーハンドリングも、通常の同期コードと同様に try…catch 構文を使って行うことができます 15。

JavaScript

// async/awaitを使った非同期処理の記述
async function runOperations(input) {
  console.log(“処理を開始します…”);
  try {
    const result1 = await asyncOperation1(input); // Promiseが解決するまで待機
    console.log(“処理1完了:”, result1);

    const result2 = await asyncOperation2(result1); // result1を使って次の処理を待機
    console.log(“処理2完了:”, result2);

    const result3 = await asyncOperation3(result2); // result2を使ってさらに次の処理を待機
    console.log(“処理3完了:”, result3);

    console.log(“最終結果:”, result3);
    return result3; // この値でPromiseが解決される

  } catch (err) {
    // いずれかのawaitでエラーが発生した場合、ここで捕捉される
    console.error(“エラー発生:”, err);
    throw err; // エラーを再スローして、呼び出し元に伝えることも可能
  }
}

// async関数を呼び出す (async関数はPromiseを返す)
runOperations(initialInput)
.then(finalResult => {
    console.log(“runOperationsが成功しました:”, finalResult);
  })
.catch(error => {
    console.error(“runOperationsが失敗しました:”, error);
  });

このコードは、Promiseチェーンの例よりもさらに直感的で、処理のステップが明確です。

コールバックからPromise、そしてasync/awaitへの進化は、JavaScriptにおける非同期処理の管理方法が、より高度な抽象化へと向かっていることを示しています 59。各ステップは、前のステップの複雑さを隠蔽し、開発者がより直感的で保守しやすいコードを書けるようにすることを目指しています。Promiseは非同期操作の結果をオブジェクトとして抽象化し、async/awaitはそのPromiseのハンドリング自体を抽象化して、より自然なコード記述を可能にしました。この進化の過程を理解することは、現代のJavaScript開発において非常に重要です。

6. 現代における使い分け

コールバック、Promise、async/await という3つの非同期処理パターンを学びましたが、現代のJavaScript開発において、これらをどのように使い分けるべきでしょうか?

コールバック関数が依然として使われる場面

Promiseやasync/awaitが主流となった現在でも、コールバック関数が依然として有効、あるいは必要となる場面があります。

  • イベントリスナー/イベントハンドラ: ブラウザのDOMイベント (addEventListener) やNode.jsの EventEmitter (.on()) など、繰り返し発生する可能性のあるイベントを処理するAPIでは、依然としてコールバック関数が主要なインターフェースです 10。イベントが発生するたびに呼び出される処理を記述するには、コールバックパターンが自然です。
  • 同期的な反復処理: Array.prototype.forEach, map, filter, reduce といった、配列などのコレクションを操作するための高階関数では、各要素に対する処理を同期的なコールバック関数として渡します 8。これは関数型プログラミングのスタイルであり、非同期処理とは異なります。
  • 非常にシンプルな非同期API: 処理が単純で、ネストが発生しないようなごく簡単な非同期処理の場合、Promiseやasync/awaitの準備が冗長に感じられるかもしれません。ただし、Node.jsでは util.promisify を使ってコールバックベースの関数を簡単にPromise化できるため 43、この理由でコールバックを選ぶ場面は減っています。
  • レガシーコードや古いライブラリ: 既存のコードベースや、更新されていない古いライブラリでは、コールバック関数が主要な非同期処理の方法として使われている場合があります 43。これらのコードと連携するためには、コールバック関数を理解し、使用する必要があります。
  • 複数回呼び出されるコールバック: 非同期処理の完了時だけでなく、処理の途中経過を通知するなど、一度の操作で複数回コールバックが呼び出されるような特殊なケースでは、Promiseよりもコールバックの方が適している場合があります 55

Promiseとasync/awaitが推奨される場面

一方で、現代の多くの非同期処理においては、Promise、特にasync/awaitの使用が強く推奨されます。

  • ほとんどの新しい非同期コード: 新規に開発する非同期処理、特に複数のステップが絡む場合や、エラーハンドリングが重要な場合には、Promise、とりわけasync/awaitが標準的な選択肢となります 43
  • 複雑な非同期フローの管理: 複数の非同期処理が順番に依存している場合、条件分岐を含む場合、エラーを適切に伝播させたい場合など、複雑な制御フローを持つ非同期処理は、async/awaitを使うことでコードが格段にシンプルになり、管理しやすくなります 49
  • Node.js開発: データベースアクセス、外部API呼び出し、ファイルI/Oなど、Node.jsにおけるサーバーサイド開発では、非同期処理が頻繁に登場します。現代のNode.js開発では、これらの処理にPromiseやasync/awaitを用いるのが一般的です 42。Node.jsのコアモジュールにも fs.promises のようにPromiseベースのAPIが提供されています 42
  • 可読性と保守性の向上: async/awaitは、非同期コードを同期コードのように記述できるため、コードの意図が伝わりやすく、他の開発者が理解したり、将来的に修正したりするのが容易になります 49

適切なアプローチの選択

では、具体的にどのアプローチを選ぶべきか、いくつかの指針を示します。

  • 基本方針: 複数の非同期処理を順番に実行する必要がある場合は、可読性の観点から async/await を第一候補とします 43
  • Promiseの直接利用: async/await が使えない環境(非常に古いブラウザや、モジュール外のトップレベルスコープなど 48)や、Promiseチェーンの記述の方が特定の処理(例えば、動的な処理の組み立て)に適していると感じる場合には、.then() / .catch() を直接使います 56
  • 並行処理: 複数の非同期処理を同時に開始し、すべての完了を待ちたい場合は、Promise.all() を使用します 48。async/await で独立した処理を単純に await で繋げてしまうと、不必要に直列実行となり、パフォーマンスが悪化する可能性があります 43
  • コールバックの維持: イベントリスナーのようにAPIがコールバックを要求する場合や、forEach のような同期的な高階関数を使う場合は、コールバック関数を使用します 30

結局のところ、「常にこれがベスト」という単一の答えはありません。async/awaitが多くの場面で可読性を向上させる強力なツールであることは間違いありませんが、それが常に最適な選択とは限りません。APIのインターフェース、実現したい制御フロー(直列か並列か、一度きりか繰り返し発生するか)、そしてチームのコーディング規約などを考慮して、最も状況に適したアプローチを選択することが重要です 21

比較表

これまでの内容をまとめた比較表を以下に示します。

特徴 (Feature)コールバック (Callback)Promiseasync/await
構文 (Syntax)func(…, (err, data) => {}).then().catch()try { await… } catch {}
可読性 (Readability)ネストが深くなりがち (Callback Hell) 43フラットなチェーンで改善 51同期処理に近く、非常に高い 51
エラー処理 (Error Handling)エラーファースト規約、手動チェック 20.catch() / onRejected コールバック 51try…catch ブロック 51
主な用途 (Primary Use Case)イベント処理、単純/古い非同期API、同期反復 10非同期処理の連鎖、エラー処理の改善 48複雑な逐次非同期処理、最高の可読性 43
基盤 (Underlying Mechanism)関数の受け渡しPromise オブジェクト 46Promise上の構文糖衣 54

この表は、各アプローチの主な違いを理解し、状況に応じた選択を行うための参考としてください。

7. まとめ:コールバック関数を理解し、使いこなすために

この記事では、JavaScriptにおけるコールバック関数の基本から始め、その利用シナリオ、課題であるコールバック地獄、そして代替技術であるPromiseとasync/await、さらには現代における使い分けまでを解説してきました。

学びの振り返り

私たちは、コールバック関数が単に「他の関数に渡される関数」であるという基本的な定義からスタートしました。そして、それがJavaScriptの関数が第一級オブジェクトであるという特性によって可能になっていること、そして柔軟な処理のカスタマイズ、イベント処理、非同期処理の実現に不可欠な役割を果たしてきたことを見てきました。

しかし、非同期処理が連続する場合、コールバック関数は「コールバック地獄」という可読性・保守性の問題を露呈しました。この問題を解決するために、非同期処理の結果をオブジェクトとして表現する Promise が登場し、.then() によるチェーンによって、よりフラットで管理しやすいコード構造を可能にしました。

さらに、Promiseを基盤としつつ、非同期コードをあたかも同期コードのように書けるようにした async/await が登場し、特に複雑な非同期フローにおける可読性を劇的に向上させました。

重要なポイント

  • コールバック関数は、関数を引数として渡す基本的なパターンであり、イベント処理や(特に初期の)非同期処理の基礎です。
  • コールバック地獄は、依存関係のある非同期処理をコールバックのネストで実装する際に生じる、可読性・保守性の問題です。
  • Promiseは、非同期処理の最終的な結果を表すオブジェクトであり、.then() や .catch() を用いたチェーンによってコールバック地獄を解消します。
  • async/awaitは、Promiseをより直感的に扱うための構文糖衣であり、try…catch と組み合わせることで、同期処理に近いスタイルで非同期コードを記述できます。
  • 現代の開発では、多くの場合async/awaitが好まれますが、APIの仕様(イベントリスナーなど)や必要な制御フロー(並列処理など)に応じて、Promiseやコールバック関数も依然として重要な役割を担っています。文脈に応じた適切なツールの選択が鍵となります。

現代的な視点と今後の展望

async/awaitが現代の非同期処理の主流であることは確かですが 43、その基盤であるPromise、そしてさらにその前身であるコールバック関数の仕組みを理解することは、依然として非常に重要です。なぜなら、古いコードや特定のAPIを扱う場面に遭遇する可能性があるだけでなく、async/awaitが内部でどのように動作しているか(Promiseに基づいていること)を理解することで、より深いレベルでのデバッグやパフォーマンスチューニングが可能になるからです。

JavaScriptにおける非同期処理のパターンは、開発者がより効率的かつ直感的にコードを書けるように進化し続けてきました 59。コールバックからPromise、そしてasync/awaitへの道のりは、その進化の証です。この進化の過程を理解することは、現在のツールを最大限に活用し、将来登場するかもしれない新しい非同期パターンにも適応していくための強固な基盤となるでしょう。Node.jsにおけるStreamやWorker Threadsのような、特定の高度な非同期処理ニーズに応えるための専門的なツールも存在します 43

非同期プログラミングは、現代のWeb開発、特にインタラクティブなフロントエンドや高性能なバックエンドを構築する上で避けては通れない道です。この記事で学んだ知識を基に、実際にコードを書き、様々なパターンを試してみることで、コールバック関数、Promise、そしてasync/awaitを自信を持って使いこなせるようになることを願っています。

引用文献

  1. e-words.jp, 4月 11, 2025にアクセス、 https://e-words.jp/w/%E3%82%B3%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF%E9%96%A2%E6%95%B0.html#:~:text=%E3%82%B3%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF%E9%96%A2%E6%95%B0%EF%BC%88callback%20function,%E3%82%88%E3%81%86%E3%81%AB%E5%AE%9F%E8%A1%8C%E3%81%95%E3%82%8C%E3%82%8B%E3%80%82
  2. 【JavaScriptの超基本】コールバック関数について簡単に解説 – Qiita, 4月 11, 2025にアクセス、 https://qiita.com/ta1fukumoto/items/1d2dc5bcf4ef0ff74eaa
  3. JavaScriptのコールバック関数の簡単な例 – Zenn, 4月 11, 2025にアクセス、 https://zenn.dev/kota1234/articles/ce25d7cc6794d7
  4. Callback function – MDN Web Docs Glossary: Definitions of Web-related terms, 4月 11, 2025にアクセス、 https://udn.realityripple.com/docs/Glossary/Callback_function
  5. コールバック関数とは?どういう時に使うの? – フルスタックエンジニアのノウハウ, 4月 11, 2025にアクセス、 https://blog.senseshare.jp/callback-function.html
  6. コールバック関数とは?【分かりやすい解説シリーズ #40】【プログラミング】 – YouTube, 4月 11, 2025にアクセス、 https://m.youtube.com/watch?v=WaDcAQHOwYw&t=0s
  7. 「コールバック」関数の意味するところ #関数型プログラミング – Qiita, 4月 11, 2025にアクセス、 https://qiita.com/s_9_i/items/0e4d0198e1cae83895ba
  8. Callback function (コールバック関数) – MDN Web Docs 用語集: ウェブ関連用語の定義, 4月 11, 2025にアクセス、 https://developer.mozilla.org/ja/docs/Glossary/Callback_function
  9. [JavaScript] コールバック関数とは #TypeScript – Qiita, 4月 11, 2025にアクセス、 https://qiita.com/Jochun/items/00f629b12b37417f8418
  10. (補遺)なぜコールバック関数をカッコ () 付きで渡すとダメなのか? – Zenn, 4月 11, 2025にアクセス、 https://zenn.dev/sprout2000/books/76a279bb90c3f3/viewer/appendix02
  11. コールバック関数 (callback functions) – サバイバルTypeScript, 4月 11, 2025にアクセス、 https://typescriptbook.jp/reference/functions/callback-functions
  12. コールバック (情報工学) – Wikipedia, 4月 11, 2025にアクセス、 https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF_(%E6%83%85%E5%A0%B1%E5%B7%A5%E5%AD%A6)
  13. Callback関数を知らん人がまず理解すべきことのまとめ。, 4月 11, 2025にアクセス、 http://shiroibanana.blogspot.com/2012/09/callback.html
  14. 同期処理と非同期処理[Promiseオブジェクト] – ディープロ, 4月 11, 2025にアクセス、 https://diveintocode.jp/blogs/Technology/SyncAndAsync
  15. JavaScriptにおける同期処理と非同期処理の理解: ECサイトの例を交えて – Zenn, 4月 11, 2025にアクセス、 https://zenn.dev/h_tatsuru/articles/28149eac34d55c
  16. Promiseとasync/awaitで非同期処理をしてみよう – cybozu developer network, 4月 11, 2025にアクセス、 https://cybozu.dev/ja/tutorials/hello-js/promise/
  17. わかりやすく解説!コールバック関数の理解とその使い方 #JavaScript – Qiita, 4月 11, 2025にアクセス、 https://qiita.com/nakajima417/items/4d0c2d46ff82351549e6
  18. 【JavaScript入門】初めてのコールバック関数の書き方と応用例! | 侍エンジニアブログ, 4月 11, 2025にアクセス、 https://www.sejuku.net/blog/258409
  19. JavaScriptの「コールバック関数」とは一体なんなのか – Subterranean Flower, 4月 11, 2025にアクセス、 https://sbfl.net/blog/2019/02/08/javascript-callback-func/
  20. 【JavaScript】非同期処理について(callback) – SHIFT Group 技術ブログ, 4月 11, 2025にアクセス、 https://note.shiftinc.jp/n/n1d20204096f7
  21. JavaScriptの非同期処理を理解する その1 〜コールバック編〜 | さくらのナレッジ, 4月 11, 2025にアクセス、 https://knowledge.sakura.ad.jp/24888/
  22. 非同期処理 (2): Javascript Callback関数について – Zenn, 4月 11, 2025にアクセス、 https://zenn.dev/redpanda/articles/9df14f1c76227d
  23. JavaScript初級講座: 非同期処理とNode.js入門 | LOGICALYZE Web Lab, 4月 11, 2025にアクセス、 https://logicalyze.com/re-introduction-javascript-2/
  24. JavaScriptにおける非同期処理とは #フロントエンド – Qiita, 4月 11, 2025にアクセス、 https://qiita.com/Hirohana/items/6a7fd4c7f4c9ccb1f6da
  25. Callback Functions(コールバック関数) を分かりやすく解説 – JamstackとNext.jsをTypeScriptで学ぶ。 – 株式会社コムテは, 4月 11, 2025にアクセス、 https://www.commte.co.jp/learn-nextjs/Callback-Functions
  26. JavaScriptの高度な関数:クロージャとコールバック | WEBデザインMATOME, 4月 11, 2025にアクセス、 https://teach.web-represent.link/closures-and-callbacks/
  27. コールバック関数について書き留める – Zenn, 4月 11, 2025にアクセス、 https://zenn.dev/yy_phoenix/articles/df2f6157c0700f
  28. 高階関数・コールバック関数とは? – OZの教える×プログラミング成長記, 4月 11, 2025にアクセス、 https://oz006.com/callback-function/
  29. コールバック関数とは何ぞや? – Zenn, 4月 11, 2025にアクセス、 https://zenn.dev/tukiyubi/articles/12b556d7c0fcc8
  30. JavaScriptではコールバック関数はすべて非同期処理になるのか? – スタック・オーバーフロー, 4月 11, 2025にアクセス、 https://ja.stackoverflow.com/questions/95580/javascript%E3%81%A7%E3%81%AF%E3%82%B3%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF%E9%96%A2%E6%95%B0%E3%81%AF%E3%81%99%E3%81%B9%E3%81%A6%E9%9D%9E%E5%90%8C%E6%9C%9F%E5%87%A6%E7%90%86%E3%81%AB%E3%81%AA%E3%82%8B%E3%81%AE%E3%81%8B
  31. JavaScriptの必須知識 – Promiseとasync/awaitの理解 – 英語xプログラミング, 4月 11, 2025にアクセス、 https://navigate-online-store.com/about-promise/
  32. JavaScriptの同期処理と非同期処理をやさしく解説します。 – アプリコットデザイン, 4月 11, 2025にアクセス、 https://apricot-design.com/staffblog/javascript%E3%81%AE%E5%90%8C%E6%9C%9F%E5%87%A6%E7%90%86%E3%81%A8%E9%9D%9E%E5%90%8C%E6%9C%9F%E5%87%A6%E7%90%86%E3%82%92%E3%82%84%E3%81%95%E3%81%97%E3%81%8F%E8%A7%A3%E8%AA%AC%E3%81%97%E3%81%BE%E3%81%99/
  33. 関数 – JavaScript – MDN Web Docs, 4月 11, 2025にアクセス、 https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Functions
  34. Node.jsの非同期I/Oについて調べてみた | Recruit Tech Blog, 4月 11, 2025にアクセス、 https://techblog.recruit.co.jp/article-1115/
  35. 【同期・非同期】PHPとNode.jsの違いについて – エンジニアBLOG, 4月 11, 2025にアクセス、 https://blog.cloudsmith.co.jp/2023/03/526/
  36. Node.jsがとっつきにくいと思ったら ~非同期処理とは?~【初心者向け】 #ノンブロッキング – Qiita, 4月 11, 2025にアクセス、 https://qiita.com/SpenelopeS/items/4343eec9bc4d383b6874
  37. Node.jsの役に立つお話(主にノンブロッキングIO, 非同期処理) | KCS – Keio Computer Society, 4月 11, 2025にアクセス、 https://old.kcs1959.jp/archives/5228/general/node-js%E3%81%AE%E5%BD%B9%E3%81%AB%E7%AB%8B%E3%81%A4%E3%81%8A%E8%A9%B1%E4%B8%BB%E3%81%AB%E3%83%8E%E3%83%B3%E3%83%96%E3%83%AD%E3%83%83%E3%82%AD%E3%83%B3%E3%82%B0io-%E9%9D%9E%E5%90%8C%E6%9C%9F
  38. JavaScriptの非同期処理、理解の糸口となるニュアンスを分かりやすく紹介 〜コールバック, async/await, I/O 〜|watanabe_kf1983 – note, 4月 11, 2025にアクセス、 https://note.com/watanabe_kf1983/n/n6df82d96207a
  39. [Node.js] 非同期型イベント駆動とは 〜 JSおくのほそ道 #001 – Qiita, 4月 11, 2025にアクセス、 https://qiita.com/hosomichi/items/16597eaabb1226975c51
  40. 【初心者向け】Node.jsとは?を初心者ながらまとめてみた #非同期処理 – Qiita, 4月 11, 2025にアクセス、 https://qiita.com/_tsuru/items/e16db3d95b72cdb2a48d
  41. Node.js の非同期処理の仕組み, 4月 11, 2025にアクセス、 https://tech-blog.lakeel.com/n/nad1e70679a96
  42. Node.jsでの非同期プログラミング: コールバックからPromisesへ | WEBデザインMATOME, 4月 11, 2025にアクセス、 https://teach.web-represent.link/callbacks-to-promises/
  43. Node.js 非同期処理全8種類まとめ 2025年版 – Zenn, 4月 11, 2025にアクセス、 https://zenn.dev/aircloset_dev/articles/66e0d2104e43dc
  44. Node.jsのFile System APIで同期処理と非同期処理の使いわけ – Qiita, 4月 11, 2025にアクセス、 https://qiita.com/am_/items/6a87a5dbf6d4ab328da3
  45. JSの非同期処理を理解するために必要だった知識と学習ロードマップ – Zenn, 4月 11, 2025にアクセス、 https://zenn.dev/estra/articles/js-async-programming-roadmap
  46. [Javascript]コールバックヘルからの帰還。Promise/Async/Awaitを考えるよ。 – Qiita, 4月 11, 2025にアクセス、 https://qiita.com/monsoonTropicalBird/items/2e002a31c261433047e5
  47. 【入門】『スーパーマリオ』で学ぶ、JavaScriptの非同期処理 – Zenn, 4月 11, 2025にアクセス、 https://zenn.dev/nameless_sn/articles/javascript_async_tutorial
  48. 如何使用Promise – 学习Web 开发| MDN, 4月 11, 2025にアクセス、 https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Extensions/Async_JS/Promises
  49. 异步解决方案—-Promise与Await · Issue #13 · ljianshu/Blog – GitHub, 4月 11, 2025にアクセス、 https://github.com/ljianshu/Blog/issues/13
  50. コールバックで副作用となる非同期処理 – Zenn, 4月 11, 2025にアクセス、 https://zenn.dev/estra/books/js-async-promise-chain-event-loop/viewer/10-epasync-dont-use-side-effect
  51. JavaScriptの非同期処理: async/awaitの基礎とその利便性について – 株式会社一創, 4月 11, 2025にアクセス、 https://www.issoh.co.jp/tech/details/3691/
  52. Promiseとasync/awaitの違い – Zenn, 4月 11, 2025にアクセス、 https://zenn.dev/445/articles/99e6a9fb75787d
  53. JavaScript 中的async/await 是什么?和promise 有什么差别? – ExplainThis, 4月 11, 2025にアクセス、 https://www.explainthis.io/zh-hans/swe/async-await
  54. Promise chain から async 関数へ|イベントループとプロミスチェーンで学ぶJavaScriptの非同期処理 – Zenn, 4月 11, 2025にアクセス、 https://zenn.dev/estra/books/js-async-promise-chain-event-loop/viewer/14-epasync-chain-to-async-await
  55. 非同期処理の使い分け コールバック関数,Promise,async/await – Qiita, 4月 11, 2025にアクセス、 https://qiita.com/yusuke2310/items/afc8ec9f16419f027904
  56. async/await – 现代JavaScript 教程, 4月 11, 2025にアクセス、 https://zh.javascript.info/async-await
  57. Promise,async/await – 现代JavaScript 教程, 4月 11, 2025にアクセス、 https://zh.javascript.info/async
  58. JavaScriptの基礎 async/awaitを理解しよう – エンベーダー, 4月 11, 2025にアクセス、 https://envader.plus/article/472
  59. 重构:从Promise到Async/Await | Fundebug博客- 一行代码搞定BUG监控, 4月 11, 2025にアクセス、 https://blog.fundebug.com/2017/12/13/reconstruct-from-promise-to-async-await/
  60. async function – JavaScript – MDN Web Docs – Mozilla, 4月 11, 2025にアクセス、 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次