コールバック関数とは何か
定義
「他の関数に引数として渡され、後から呼び出される関数」 がコールバックです。
例(同期):
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i); // ← 渡されたコールバックを呼んでいる
}
}
repeat(3, (i) => {
console.log("回数:", i);
});ここで、
actionがコールバック関数repeatが「コールバックを受け取って実行する関数」
ポイント:
- コールバック自体は「同期」「非同期」を問わない一般概念
- ただし「コールバック」と聞いたとき、多くの場合は 非同期コールバック の文脈を指すことが多いです
非同期コールバックの例
setTimeout
console.log("A");
setTimeout(() => {
console.log("timeout");
}, 1000);
console.log("B");setTimeout(callback, delay)に渡した関数が、「delayミリ秒後に」呼ばれる- イベントループ的には「delay後にタスクキューへコールバックが積まれる」
出力順:
A
B
(約1秒後)
timeoutDOM イベントハンドラ
button.addEventListener("click", () => {
console.log("clicked!");
});- クリックされたタイミングでコールバックが呼び出される
Node.jsに多い「エラーファーストコールバック」
Node.js の古い(コールバックベースの)APIでよく見るパターン:
fs.readFile("data.txt", "utf8", (err, data) => {
if (err) {
console.error("読み込み失敗:", err);
return;
}
console.log("読み込み成功:", data);
});この (err, data) => { ... } 型を エラーファーストコールバック と呼びます。
ルール:
- 第1引数:エラー(エラーがなければ
nullやundefined) - 第2引数以降:成功時の結果
メリット:
- エラーと成功結果が1つのコールバックにまとまる
デメリット:
- ネストが深くなると、
if (err)だらけになりがち - 途中でエラーが起こったときに「どこで処理済みにしたか」が見えにくい
コールバックの問題点(なぜ Promise が必要になったか)
コールバック地獄
複数の非同期処理を「順番に」行いたいとき、素直に書くとこうなりがちです:
doA((err, resultA) => {
if (err) { handleError(err); return; }
doB(resultA, (err, resultB) => {
if (err) { handleError(err); return; }
doC(resultB, (err, resultC) => {
if (err) { handleError(err); return; }
doD(resultC, (err, resultD) => {
if (err) { handleError(err); return; }
console.log("最終結果:", resultD);
});
});
});
});問題点:
- インデントが右にずれてネストが深くなる
- エラー処理のブロックが何度も出てきて、見通しが悪くなる
- 途中で「キャンセルしたい」「途中の結果をキャッシュしたい」などの要件が来ると、メンテナンスが大変
エラーハンドリングの難しさ
- あるコールバックで例外が投げられた場合、どこまで伝播するのか分かりにくい
- try/catch で囲んでも、そのスコープ外で発生する非同期エラーは捕まえられない
→ コールバック内でthrowしても、「コールバックの実行時点にはもうtryが終わっている」からです。
try {
setTimeout(() => {
throw new Error("これはcatchされない");
}, 0);
} catch (e) {
console.log("ここには来ない");
}制御フローが分かりづらい
- 並列実行(「AとBを同時にやって、両方終わったらC」など)をコールバックで書くと、制御フローがとにかく複雑
- タイムアウト、リトライ、キャンセルなどの高度なロジックも、コールバックだけで綺麗に書くのは難しい
Promise は、これらの問題を「ある程度」解決するために導入された仕組みです。
Promise の基本概念
Promise の「状態」
Promise は「非同期処理の最終結果を表すオブジェクト」で、
必ず次のいずれかの状態を取ります。
pending:保留中(まだ結果が出ていない)fulfilled:成功(値を持って完了)rejected:失敗(理由(エラー)を持って完了)
状態は 一方向にしか進まない:
pending → fulfilledpending → rejected
一度 fulfilled または rejected になったら、それ以降は変わらない。
Promise オブジェクトの作り方
基本形:
const p = new Promise((resolve, reject) => {
// ここが「executor(実行関数)」
// 非同期処理をここに書く
// 成功時
resolve(value);
// 失敗時
reject(error);
});単純な例(1秒後に成功するPromise):
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("OK");
}, 1000);
});
p.then((value) => {
console.log("成功:", value); // "成功: OK"
}).catch((err) => {
console.error("失敗:", err);
});then / catch / finally
p.then(onFulfilled, onRejected?)- 成功時:
onFulfilled(value) - 失敗時:第2引数の
onRejected(reason)(あまり使われず、catchに任せることが多い)
- 成功時:
p.catch(onRejected)- 上流のエラー(reject や throw)をまとめて受ける
p.finally(onFinally)- 成功・失敗どちらでも必ず呼ばれる
- ローディングUIを消したり、リソース解放などに便利
doAsync()
.then(result => {
console.log("成功:", result);
})
.catch(err => {
console.error("エラー:", err);
})
.finally(() => {
console.log("完了(成功・失敗どちらでも)");
});Promise チェーンの挙動
then は「新しい Promise を返す」
const p = doAsync(); // Promise
const p2 = p.then(result => {
return result * 2;
});
p2.then(value => {
console.log(value);
});thenの戻り値p2は 常に新しい Promisethenのコールバックが返した値が、次の Promise の結果になる
3パターン押さえておくと楽です:
- 値を返す
.then(value => value * 2)→ その値でfulfilledされた Promise を返す - Promise を返す
.then(value => otherAsync(value))→ その Promise が落ち着く(fulfill/reject)まで、チェーン全体がそれに追従する - 例外を投げる or
throwする.then(value => { if (value < 0) { throw new Error("負の値はNG"); } return value; })→ 即座に reject 状態の Promise を返す(catchで拾える)
エラー伝播のイメージ
doAsync1()
.then(result1 => {
return doAsync2(result1);
})
.then(result2 => {
return doAsync3(result2);
})
.then(result3 => {
console.log("全部成功:", result3);
})
.catch(err => {
console.error("どこかでエラー:", err);
});doAsync1,doAsync2,doAsync3のどこかでrejectorthrowされたら、- そこから先の
.thenはスキップされ、一番近いcatchに飛んでくる
これが「コールバック地獄」から解放されるポイントです。
コールバックスタイルを Promise に変換する
例:setTimeout をPromise化
function delay(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
delay(1000).then(() => {
console.log("1秒経過");
});- コールバックを受け取る
setTimeoutを、
「msミリ秒後に resolve される Promise」としてラップしている - これで
delay(1000)を.then/awaitで扱えるようになる
エラーファーストコールバックをPromise化
Node 風のAPI:
function readFileCb(path, callback) {
// callback(err, data)
}これを Promise で包む:
function readFilePromise(path) {
return new Promise((resolve, reject) => {
readFileCb(path, (err, data) => {
if (err) {
reject(err); // 失敗 → reject
} else {
resolve(data); // 成功 → resolve
}
});
});
}使い方:
readFilePromise("data.txt")
.then(data => {
console.log("成功:", data);
})
.catch(err => {
console.error("エラー:", err);
});- コールバックスタイルのAPIを Promise 化することを promisify と呼ぶ
- Node.js 公式には
util.promisifyというユーティリティ関数もある
コールバック vs Promise
新規コードを書くなら基本 Promise / async/await
- 非同期処理の読みやすさ・エラーハンドリングの楽さを考えると、今から新しく書く場合はほぼ Promise(+ async/await)一択です
- コールバックはイベントハンドラ(DOMの
addEventListener)などで自然に使われる場面だけに絞ると良い
既存のコールバックAPIに遭遇したら
- まずは そのままコールバックで使っても問題ないかを考える
- 単発で使うだけなら、そのままでもOK
- 何度も使ったり、他の Promise ベースの処理と組み合わせたくなったら、
→ さきほどのように ラッパー(promisify関数) を作る
イベントは「ストリーム」、Promiseは「一回きり」
- ボタンクリックなど 何度も起こるもの → イベントリスナー(コールバック)が本職
- HTTP リクエスト結果など 一度きり の非同期 → Promise/async が本職
この区別を意識しておくと、設計がスッキリします。
Promise の静的メソッド
Promise.resolve:値を「すでに成功したPromise」に包む
基本の挙動
const p = Promise.resolve(42);
p.then((value) => {
console.log(value); // 42
});Promise.resolve(x)は 「xで成功した Promise」 を返します。- 引数なしの場合は、
undefinedで成功した Promise を返します。
Promise.resolve().then(v => console.log(v)); // undefined
Promise.resolve("OK").then(v => console.log(v)); // "OK"非同期処理はしていなくても、“Promiseな値” として扱える のがポイントです。
new Promise((resolve) => resolve(value)) との違い
次の2つは、ほとんど同じに見えます:
const p1 = Promise.resolve(123);
const p2 = new Promise((resolve) => {
resolve(123);
});意味としては どちらも「123で成功したPromise」 ですが、Promise.resolve の方が 簡潔で意図が分かりやすいです。
- 非同期処理を新しく書いているわけではない
- 単に、値をPromiseにしたいだけ
というときは、Promise.resolve を使うのが一般的です。
Promise.resolve の「同化(assimilation)」ルール
Promise.resolve は、渡したものが何かによって挙動が少し変わります:
- すでに Promise の場合
const p = Promise.resolve(42);const p2 = Promise.resolve(p);console.log(p === p2); // true(同じ Promise をそのまま返す)
⇒ Promise を二重に包まない。「そのまま返すだけ」 - 「then メソッドを持つオブジェクト」の場合(thenable)
const thenable = { then(resolve, reject) { resolve("from thenable"); } };Promise.resolve(thenable).then((v) => { console.log(v); // "from thenable" });
⇒thenを呼んで、その結果に「同化」します。 - それ以外の値(普通の値)の場合 → 単純に
valueとしてfulfilledの Promise を返す
Promise.resolve(10); // 10で成功
Promise.resolve("hello"); // "hello"で成功
Promise.resolve(null); // nullで成功
Promise.resolve({}); // オブジェクトで成功よくある使い方
- 「同期も非同期もまとめて Promise として扱いたい」 ときのラップに使う
function maybeAsync(flag) {
if (flag) {
return fetch("/data.json"); // Promise
} else {
return { cached: true }; // 普通の値
}
}
// 受け取る側
Promise.resolve(maybeAsync(trueOrFalse))
.then((result) => {
// result は「Promiseでも値でも」同じように扱える
});Promise.reject:失敗したPromiseを簡単に作る
基本の挙動
const p = Promise.reject(new Error("エラー"));
p.catch((err) => {
console.error(err); // Error: エラー
});Promise.reject(reason)は 「reasonによって失敗した Promise」 を返します。
Promise.resolve の 失敗版 だと思えばOKです。
new Promise((_, reject) => reject(reason)) との違い
const p1 = Promise.reject(new Error("NG"));
const p2 = new Promise((resolve, reject) => {
reject(new Error("NG"));
});意味としては「同じく reject された Promise」ですが、Promise.reject の方が「新しい非同期処理を作ってるわけじゃない」とパッと見で分かりやすい
「今この関数はエラーの Promise を返したいだけ」という場面では、Promise.reject がよく使われます。
async 関数内との対応
async 関数で throw したときと、Promise.reject の関係:
async function f() {
throw new Error("NG");
}
f().catch(err => console.error(err));
// Promise.reject(new Error("NG")) とほぼ同等async 関数での throw は、暗黙に Promise.reject だと思ってよい。
Promise.all:全部成功するまで待つ
Promise.all([p1, p2, p3]);- 全ての Promise が
fulfilled→ 1つの Promise としてfulfilled- 値は
[p1の値, p2の値, p3の値]
- 値は
- 1つでも
rejected→ 即座にrejected- 最初に失敗したPromiseのエラーが
reason
- 最初に失敗したPromiseのエラーが
例
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3])
.then(values => {
console.log(values); // [1, 2, 3]
})
.catch(err => {
console.error("どれかが失敗:", err);
});使いどころ:
- 「全部揃わないと意味がない」並列処理
(ユーザー情報・設定・初期データを同時取得 など)
Promise.race:一番早いもの勝ち
Promise.race([p1, p2, p3]);- 最初に settled(成功か失敗)した Promise の結果 をそのまま返す
- それが成功なら resolve、失敗なら reject
例:タイムアウト
function delay(ms) {
return new Promise(res => setTimeout(res, ms));
}
function fetchWithTimeout(url, ms) {
const req = fetch(url);
const timeout = delay(ms).then(() => {
throw new Error("Timeout");
});
return Promise.race([req, timeout]);
}使いどころ:
- タイムアウトを実装したい
- 複数サーバに投げて「一番早いレスポンスだけ使う」
Promise.allSettled:全部の成功・失敗を知りたい
Promise.allSettled([p1, p2, p3]);- 全ての Promise が
fulfilledかrejectedになるまで 待つ - その後、必ず resolve される
- 結果は各要素ごとに:
[ { status: "fulfilled", value: ... }, { status: "rejected", reason: ... }, ... ]
例
const p1 = Promise.resolve(1);
const p2 = Promise.reject(new Error("NG"));
const p3 = Promise.resolve(3);
Promise.allSettled([p1, p2, p3]).then(results => {
console.log(results);
});使いどころ:
- 一部成功・一部失敗でも、全部の結果をまとめて処理したい
- バッチ処理、まとめアップロードなど
Promise.all だと「一つでも失敗したら全体が reject」なので、
「全体としては成功・失敗関係なく結果を集計したい」ときは allSettled が向いています。
Promise.any:どれか1つでも成功すればOK
Promise.any([p1, p2, p3]);- 最初に
fulfilledになった Promise の値を resolve - 全部
rejectedだった場合だけ、AggregateErrorで reject
race との違い:
race:成功・失敗どちらも対象。一番早く settled したものを採用any:成功だけ対象。成功した Promise が1つでもあれば resolve
例
const p1 = Promise.reject(new Error("A"));
const p2 = Promise.resolve("B");
const p3 = Promise.resolve("C");
Promise.any([p1, p2, p3])
.then(value => {
console.log(value); // "B"(最初に成功したもの)
})
.catch(err => {
console.error("全部失敗した場合のみここに来る");
});使いどころ:
- ミラーサーバ・バックアップルートなどで、「どれか1つ成功すれば十分」 なとき
- 全滅したら初めてエラーにしたい場合
コメント