JavaScript | 非同期処理:Promise 応用 – Promise のラップ

JavaScript JavaScript
スポンサーリンク

まず「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 ステップをやってみると、
「ラップ」という言葉が、ただの概念ではなく、「書ける手癖」として自分の中に定着していくはずです。

タイトルとURLをコピーしました