Promise 判定とは何を見分けたいのか
ここでいう「Promise 判定」は、その値が「非同期処理を表す Promise なのかどうか」を見分けることです。
業務コードでは、async/await や API 呼び出し、DB アクセス、ファイル操作など、非同期処理が当たり前に出てきます。
そのときによくあるのが、「この関数は Promise を返すかもしれないし、同期値を返すかもしれない」というパターンです。
そういう関数の戻り値を扱うときに、「これは Promise なのか? それとも普通の値なのか?」を判定できると、コードをかなりきれいに書けます。
まずは「Promise とは何か」をざっくり整理する
Promise は、「今は結果がないけれど、将来どこかのタイミングで成功か失敗かが決まる値」です。
そして、「then や catch で結果を受け取れるオブジェクト」として扱われます。
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("完了");
}, 1000);
});
p.then((result) => {
console.log(result); // 1 秒後に「完了」
});
JavaScriptasync 関数も、内部的には必ず Promise を返します。
async function fetchData() {
return 42;
}
const result = fetchData(); // これは Promise
console.log(result instanceof Promise); // true
JavaScriptつまり、「非同期処理の結果を表す値=Promise」と考えておけば OK です。
instanceof Promise だけでは足りない理由
一見すると、「Promise 判定は value instanceof Promise でいいのでは?」と思うかもしれません。
const p = Promise.resolve(1);
console.log(p instanceof Promise); // true
JavaScriptこれは確かに動きますが、実務では「Promise っぽいもの(thenable)」も扱う必要が出てきます。
例えば、自作の「Promise 風オブジェクト」や、外部ライブラリが返す「then メソッドだけ持っているオブジェクト」です。
const thenable = {
then(onFulfilled) {
onFulfilled(123);
},
};
JavaScriptこれは instanceof Promise では false ですが、await したり Promise.resolve に渡したりすると、Promise と同じように扱われます。
(async () => {
const value = await thenable;
console.log(value); // 123
})();
JavaScriptこのように、「Promise かどうか」を広い意味で判定したいときは、instanceof Promise だけでは不十分です。
実務でよく使う「Promise っぽいかどうか」の判定
Promise や「Promise っぽいもの」をまとめて扱いたいとき、
よく使われるのが「thenable 判定」です。
「thenable」とは、「then メソッドを持っているオブジェクト」のことです。
Promise も thenable の一種です。
function isPromiseLike(value) {
return (
value !== null &&
(typeof value === "object" || typeof value === "function") &&
typeof value.then === "function"
);
}
JavaScriptこの関数は、「null ではなく」「オブジェクトか関数で」「then が関数として存在するもの」を true と判定します。
console.log(isPromiseLike(Promise.resolve(1))); // true
console.log(isPromiseLike({ then() {} })); // true
console.log(isPromiseLike(123)); // false
console.log(isPromiseLike(null)); // false
console.log(isPromiseLike({})); // false
JavaScript業務的には、「await できるものかどうか」「then でつなげるものかどうか」を知りたい場面が多いので、isPromiseLike のような判定がかなり役に立ちます。
「純粋な Promise だけ」を判定したい場合
一方で、「自分たちが使っているのはネイティブの Promise だけで、thenable は対象外でいい」というケースもあります。
その場合は、instanceof Promise を使ったシンプルな判定で十分です。
function isPromise(value) {
return value instanceof Promise;
}
console.log(isPromise(Promise.resolve(1))); // true
console.log(isPromise({ then() {} })); // false
console.log(isPromise(123)); // false
JavaScriptどちらを使うかは、「自分のプロジェクトで thenable をどこまで許容するか」によります。
迷ったら、まずは「広めにとらえる isPromiseLike」を用意しておき、
「ネイティブ Promise だけに絞りたい場面」が出てきたら isPromise を追加する、という順番でもいいです。
同期値と Promise を一緒に扱うためのユーティリティ
Promise 判定が本当に効いてくるのは、「同期値かもしれないし Promise かもしれない」という戻り値を扱うときです。
例えば、次のような関数を考えます。
function maybeAsync(flag) {
if (flag) {
return Promise.resolve(42);
}
return 42;
}
JavaScriptこの関数は、flag によって「同期値」か「Promise」かが変わります。
これを使う側で、毎回こう書くのは面倒です。
const result = maybeAsync(true);
if (isPromiseLike(result)) {
result.then((v) => console.log(v));
} else {
console.log(result);
}
JavaScriptそこで、「同期値でも Promise に包んでしまう」ユーティリティを用意すると、扱いが一気に楽になります。
function toPromise(value) {
return isPromiseLike(value) ? value : Promise.resolve(value);
}
const p1 = toPromise(maybeAsync(true));
const p2 = toPromise(maybeAsync(false));
Promise.all([p1, p2]).then(([v1, v2]) => {
console.log(v1, v2); // 42 42
});
JavaScriptここで isPromiseLike を使っているおかげで、「すでに Promise ならそのまま」「そうでなければ Promise.resolve で包む」という共通処理が書けます。
実務では、この「同期・非同期を一旦 Promise にそろえる」テクニックがかなり強力です。
async/await と Promise 判定の関係
async 関数は、必ず Promise を返します。
async function f() {
return 1;
}
console.log(isPromise(f())); // true
JavaScriptそのため、「この関数は async かどうか」を知りたいときも、
「戻り値が Promise かどうか」を見ればだいたい分かります。
ただし、普通の関数でも Promise を返すことはできるので、
「async かどうか」を厳密に判定するのはあまり意味がありません。
実務的には、「戻り値が Promise(または Promise っぽいもの)かどうか」を見て、
「await すべきかどうか」「then でつなぐべきかどうか」を決める、という発想で十分です。
実務での具体的な利用イメージ
フック関数が同期でも非同期でも動くようにする
例えば、「前処理フック」を受け取るユーティリティを考えます。
async function runWithHook(hook, task) {
const hookResult = hook?.();
// hook が同期でも非同期でも待てるようにする
if (isPromiseLike(hookResult)) {
await hookResult;
}
return task();
}
JavaScriptここでは、「hook が Promise を返すかもしれないし、何も返さないかもしれない」という前提で、isPromiseLike を使って「必要なら await する」という書き方をしています。
これにより、呼び出し側は「同期フック」でも「非同期フック」でも自由に書けるようになります。
runWithHook(
() => console.log("同期フック"),
() => console.log("タスク")
);
runWithHook(
async () => {
console.log("非同期フック開始");
await new Promise((r) => setTimeout(r, 1000));
console.log("非同期フック終了");
},
() => console.log("タスク")
);
JavaScriptエラーハンドリングの統一
同期処理と非同期処理をまとめて扱いたいときにも、Promise 判定+ toPromise が役立ちます。
function runSafely(fn) {
try {
const result = fn();
return toPromise(result).catch((err) => {
console.error("エラー:", err);
});
} catch (err) {
console.error("エラー:", err);
return Promise.resolve();
}
}
JavaScriptここでは、「fn が同期エラーを投げる場合」と「Promise を返して非同期エラーになる場合」を両方カバーしています。toPromise の中で isPromiseLike を使っているおかげで、「戻り値が何であっても最終的に Promise として扱える」ようになっています。
小さな練習で感覚をつかむ
最後に、手を動かして慣れるためのミニ課題を提案します。
自分で isPromise, isPromiseLike, toPromise を実装して、次の値を順番に渡してみてください。
Promise.resolve(1), new Promise(() => {}), { then() {} }, async () => {}, () => 1, 123, "abc", null, undefined
それぞれに対して、「isPromise はどう返すか」「isPromiseLike はどう返すか」「toPromise はどんな Promise を返すか」をコンソールに出してみると、
「Promise そのもの」と「Promise っぽいもの(thenable)」の違いが、かなりクリアに見えてきます。
