まず「Promise のラップ」を一言でいうと
Promise の「ラップ」とは、
「既存の処理(コールバック形式や Promise など)を “Promise を返す関数” という形に包み直して、扱いやすくすること」
だと思ってください。
たとえば、こんな目的でよく使います。
- コールバック地獄な API を、Promise で扱いやすくしたい
- もともと Promise を返す関数に「タイムアウト機能」や「リトライ機能」を足したい
- ライブラリのバラバラなインターフェースを、「Promise を返す関数」に統一したい
ここが重要です。
「Promise のラップ」とは、“Promise を返す関数を自分で定義して、既存の処理を中に隠す(包む)こと」。
外側から見たときに「いつも同じ Promise の形で使える」ようにするのが目的です。
パターン1:コールバック型 API を Promise でラップする
なぜラップするのかのイメージ
古い JavaScript の API や、昔のライブラリは、
「コールバック関数を引数に渡すスタイル」が多いです。
例えば、Node.js 風のこういう関数:
// 仮想的な非同期関数(エラー第1引数スタイル)
function readFile(path, callback) {
setTimeout(() => {
if (path === "notfound.txt") {
callback(new Error("ファイルが見つかりません"), null);
} else {
callback(null, "ファイルの中身です");
}
}, 500);
}
JavaScript使うときはこうなります。
readFile("data.txt", (err, data) => {
if (err) {
console.error("エラー:", err);
return;
}
console.log("内容:", data);
});
JavaScriptこれはこれで動きますが、
エラー処理が毎回 if (err) になる
複数の非同期処理を組み合わせるとすぐネストする
Promise.all などが使えない
といった不便があります。
そこで「Promise でラップ」して、readFilePromise(path) のように Promise を返す関数に包み直したくなります。
実際にラップしてみる
readFile をラップして、Promise 版を作ります。
function readFilePromise(path) {
return new Promise((resolve, reject) => {
readFile(path, (err, data) => {
if (err) {
reject(err); // エラーなら reject
} else {
resolve(data); // 成功なら resolve
}
});
});
}
JavaScriptこれで、
readFilePromise("data.txt")
.then((data) => {
console.log("内容:", data);
})
.catch((err) => {
console.error("エラー:", err);
});
JavaScriptというふうに、
普通の Promise と同じ書き方で扱えるようになります。
さらに、他の Promise と組み合わせて Promise.all なども使えるようになります。
ここが重要です。
「コールバック型 API を Promise でラップする」とは、
中で new Promise を作り、その executor の中で元のコールバックを呼んで、成功なら resolve、失敗なら reject してあげること。
これだけで、古い API も現代的な Promise チェーンの仲間入りができます。
パターン2:元から Promise を返す関数に「機能を足す」ラップ
目的のイメージ
すでに Promise を返す関数でも、
タイムアウトをつけたい
何回かリトライしたい
ログを追加したい
といったときに、
「元の Promise を内側で呼び出す“ラッパー関数”」 を作ります。
このときも、やることは同じです。
「外側の関数は Promise を返す」ようにして、中で元の Promise を呼びます。
例:既存の Promise にタイムアウトをつけてラップする
まず、もともとある Promise ベースの関数を仮定します。
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("サーバーからのデータ");
}, 3000); // 3秒かかる
});
}
JavaScriptこの fetchData に対して、
「2秒以上かかったらタイムアウトエラーにしたい」
という要件を、ラップで実現します。
function fetchDataWithTimeout(ms) {
return new Promise((resolve, reject) => {
const timerId = setTimeout(() => {
reject(new Error(`タイムアウト: ${ms}ms を超えました`));
}, ms);
fetchData()
.then((data) => {
clearTimeout(timerId); // タイマー解除
resolve(data); // 元の結果で resolve
})
.catch((err) => {
clearTimeout(timerId);
reject(err); // 元のエラーをそのまま投げる
});
});
}
JavaScript使い方はこうです。
fetchDataWithTimeout(2000)
.then((data) => {
console.log("成功:", data);
})
.catch((err) => {
console.error("エラー:", err.message);
});
JavaScriptここでの流れは、
外側で「タイムアウト監視用の Promise」を作る
同時に中で fetchData() を呼ぶ
先に fetchData が終われば、その結果で resolve
タイムアウト時間が先に来れば、外側を reject
という動きです。
つまり、
「既存の Promise を呼び出しつつ、外側で追加のロジック(タイマー)を巻いている」
これが「Promise をラップして機能を足す」というイメージです。
ここが重要です。
既存の Promise に機能を足したいときは、「外側で new Promise を作り、その中で元の Promise を呼び、必要なタイミングで resolve / reject を呼び直す」ことで、ラッパーを作れる。
パターン3:失敗したらリトライする Promise ラッパー
要件のイメージ
例えば、
ネットワークが一時的に不安定
1 回エラーになっても、2 回目は成功するかもしれない
最大 3 回まで再試行したい
というとき、「リトライロジック」を毎回書くのは大変です。
そこで、「Promise を返す関数に対して、共通のリトライ機能をラップした関数」を作るのが便利です。
リトライラッパーの実装例
まず、「一回分のリクエスト」を Promise で表す関数があるとします。
function unstableRequest() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const ok = Math.random() > 0.5; // 50% の確率で成功
if (ok) {
resolve("成功しました");
} else {
reject(new Error("一時的なエラー"));
}
}, 500);
});
}
JavaScriptこれに「最大 n 回までリトライするラッパー」を作ります。
function withRetry(fn, maxRetry) {
return function wrapped(...args) {
return new Promise((resolve, reject) => {
let attempt = 0;
function tryOnce() {
attempt += 1;
fn(...args)
.then((result) => {
resolve(result); // 成功したらそのまま返す
})
.catch((err) => {
if (attempt < maxRetry) {
console.warn(`失敗。リトライ ${attempt}/${maxRetry} 回目`, err.message);
tryOnce(); // もう一度
} else {
reject(err); // 規定回数を超えたらあきらめる
}
});
}
tryOnce();
});
};
}
JavaScript使うときはこうです。
const requestWithRetry = withRetry(unstableRequest, 3);
requestWithRetry()
.then((res) => {
console.log("最終的に成功:", res);
})
.catch((err) => {
console.error("リトライしてもダメだった:", err.message);
});
JavaScriptここでやっている「ラップ」は、次のように整理できます。
元の関数 fn 自体は変えない
新しく wrapped という関数を作り、その中で fn を何度も呼び直すロジックを書くwrapped は Promise を返すので、外側からは「リトライ機能付きの Promise 関数」として使える
ここが重要です。
Promise のラップを使うと、「元の関数を一切触らずに」「外から機能を追加する」ことができる。
これが「ラッパー関数」の強力なところです。
パターン4:インターフェースを統一するためのラップ
問題のイメージ
プロジェクトが大きくなると、こんなことが起きがちです。
ある API はコールバック形式
ある API は Promise を返す
ある処理は同期ですぐ値を返す
ある処理は非同期
バラバラだと、呼び出す側のコードがやたら複雑になります。
そこで、「全部 Promise を返す関数に揃える」 ためにラップを使います。
同期処理を Promise 化して揃える
例えば、こういう関数があるとします。
function getLocalConfig() {
return { theme: "dark", lang: "ja" }; // 同期で即返す
}
JavaScriptこれを「Promise を返すもの」として扱いたいなら、
呼び出し側で毎回 Promise.resolve(getLocalConfig()) としてもいいですが、
専用のラップを作っておくこともできます。
function getLocalConfigAsync() {
return Promise.resolve(getLocalConfig());
}
JavaScriptこれで、
Promise.all([getLocalConfigAsync(), fetchRemoteSettings()])
.then(([local, remote]) => {
console.log("設定:", local, remote);
});
JavaScriptのように、
同期・非同期を意識せずに「全部 Promise」として扱える ようになります。
ここが重要です。
Promise のラップは、
「同期」「コールバック」「Promise」などバラバラな世界を、
「全部 Promise を返す関数」という共通インターフェースに揃えるための道具でもある。
ラップを書くときに気をつけるポイント
できるだけ「一度だけ resolve / reject」になるようにする
new Promise の中でラップを書くときに気をつけたいのは、
resolve と reject は「必ず 1 回だけ」呼ばれるようにする
ということです。
例えば、このような間違った書き方は要注意です。
function badWrap(fn) {
return new Promise((resolve, reject) => {
fn((err, result) => {
if (err) reject(err);
resolve(result); // if をつけ忘れていると、エラー時でも resolve が呼ばれるかもしれない
});
});
}
JavaScript正しくは、こうです。
function goodWrap(fn) {
return new Promise((resolve, reject) => {
fn((err, result) => {
if (err) {
reject(err);
return;
}
resolve(result);
});
});
}
JavaScriptあるいは else を使う。
「ラップの中で resolve と reject のどちらか一方だけ必ず呼ぶ」
という感覚を持っておくと安全です。
既に Promise なのに、むやみに二重にラップしない
例えば、こういうのはあまり意味がありません。
function wrapFetch() {
return new Promise((resolve, reject) => {
fetch("/api/data")
.then(resolve)
.catch(reject);
});
}
JavaScriptこれは「fetch の Promise を、まったく同じ意味の Promise で二重に包んでいる」だけなので、
普通は fetch("/api/data") をそのまま返せば十分です。
ただし、「途中でログを入れたい」「タイムアウトをつけたい」など、
“何か追加の意味” があるときにだけラップを使う のが良いです。
ここが重要です。
ラップは「インターフェースを変える」か「機能を足す」ために使う。
意味のない二重ラップはコードを読みにくくするだけ。
初心者向け「Promise のラップ」の押さえどころ
最後に、Promise のラップを理解するうえで、本当に大事なポイントだけ整理します。
Promise のラップとは、「既存の処理を、Promise を返す関数として包み直すこと」。
目的は、古いコールバック API やバラバラな関数を、「同じ Promise インターフェース」に揃えること。
コールバック型 API をラップするときは、new Promise((resolve, reject) => { 元の関数(..., (err, result) => { if (err) reject(err); else resolve(result); }); }); という基本パターンを覚える。
既存の Promise に機能を足したいとき(タイムアウト、リトライ、ログなど)は、外側に new Promise を作り、その中で元の Promise を呼び、必要なタイミングで resolve / reject し直す。
どんなラップでも、「外側の関数は Promise を返す」という形にしておけば、呼び出し側は then / catch / Promise.all など、Promise の世界の道具を自由に使える。
ここが重要です。
Promise のラップは、「世界を Promise ベースに揃える」ための技術です。
“外から包んで Promise にする”という発想さえ掴めば、古い API も、既存の Promise も、一気に扱いやすい形に整えることができます。
おすすめの練習としては、
1つ目に、setTimeout を使った「コールバック型関数」を自分で作ってみる
それを「Promise を返す関数」にラップして書き直してみる
2つ目に、fetch など既に Promise な関数に、「タイムアウトをつけるラッパー」を自分で書いてみる
この 2 ステップをやってみると、
「ラップ」という言葉が、ただの概念ではなく、「書ける手癖」として自分の中に定着していくはずです。
