とほほのPromise入門

トップ > アラカルト > とほほのPromise入門

目次

Promiseとは

Promise は、JavaScript や Node.js において、非同期処理のコールバック関数をエレガントに記述するための仕組みです。英語の promise は、「制約」、「保障」などの意味を持ちます。Promise は、Chrome 63, Firefox 58, Safari 11.1, Edge 18, Node.js 4.* から利用可能です。IE11 ではサポートされていません。

コールバック地獄

JavaScript や Node.js では、ブロックする(処理が終わるまで待ち合わせる)関数よりも、非同期関数(処理の完了を待たず、処理が完了した時点でコールバック関数が呼び出される)の方が多様されます。ここで、例えば、膨大な演算(実は単に元の数を2倍するだけ)を行う非同期関数 aFunc1() があるとします。下記は、100の2倍を求める非同期関数の使用例です。

JavaScript
// 引数を2倍にする非同期関数
function aFunc1(data, callback) {
    setTimeout(function() {
        callback(data * 2);
    }, Math.random() * 1000);
}
JavaScript
function sample_callback() {
    // 非同期関数を用いて100の2倍を求める
    aFunc1(100, function(value) {
        console.log(value);      // => 200
    });
}

単純に非同期関数を1回だけ呼び出すのであれば、上記で問題ありませんが、1回目で得られた値を用いて、aFunc1() を2度、3度呼び出そうとすると、下記の様な実装になります。

JavaScript
function sample_callback_hell() {
    aFunc1(100, function(data) {
        console.log(data);                  // => 200
        aFunc1(data, function(data) {
            console.log(data);              // => 400
            aFunc1(data, function(data) {
                console.log(data);          // => 800
            });
        });
    });
}

呼び出す回数に比例してコールバックのネストが深くなります。これを、「コールバック地獄」と呼びます。

タイミング問題

非同期関数はまた、処理の順序を制御できないという問題も含みます。下記の例では、100の2倍、200の2倍、400の2倍を求めようとしたにも関わらず、処理結果は 200, 400, 800 だったり、800, 200, 400 など、結果処理が順不同となるという問題があります。

JavaScript
function sample_timing_problem() {
    aFunc1(100, function(data) {
        console.log(data);      // => 200
    });
    aFunc1(200, function(data) {
        console.log(data);      // => 400
    });
    aFunc1(400, function(data) {
        console.log(data);      // => 800
    });
}

Promiseによる解決

これらの問題を解決するために考案されたのが Promise です。Promise は、約束、誓約、保証などの意味を持ちます。Promise は、待機(pending)、成功(fulfilled)、失敗(rejected)の3値を持つオブジェクトです。前述の非同期関数 aFunc1() を Promise を用いて書き直すと下記の様になります。処理を行う関数を引数とした Promise オブジェクトを返却するように修正します。

JavaScript
function aFunc2(data) {
    return new Promise(function(callback) {
        setTimeout(function() {
            callback(data * 2);
        }, Math.random() * 1000);
    });
}

Promise オブジェクトは then(ok_callback, ng_callback) というメソッドを持ちます。then() は、Promise が成功または失敗になるまで処理を受け流し、成功時に ok_callback を、失敗時に ng_callback をコールバック関数として呼び出します。

JavaScript
function sample_promise() {
    aFunc2(100).then(function(data) {
        console.log(data);      // => 200
    });
}

アロー関数を用いると、次のようにも記述できます。

JavaScript
function sample_promise2() {
    aFunc2(100).then((data) => {
        console.log(data);      // => 200
    });
}

さらに処理を継続するには、下記の様にします。

JavaScript
function sample_promise3() {
    aFunc2(100).then((data) => {
        console.log(data);      // => 200
        return aFunc2(data);
    })
    .then((data) => {
        console.log(data);      // => 400
        return aFunc2(data);
    })
    .then((data) => {
        console.log(data);      // => 800
    });
}

エラー処理

Promise のエラー処理について考察します。下記は、約 30% の確率でエラーとなる Promise 非同期関数です。

JavaScript
function aFunc3(data) {
    return new Promise(function(okCallback, ngCallback) {
        setTimeout(function() {
            if (Math.random() < 0.30) {
                ngCallback(new Error('ERROR!'));
            } else {
                okCallback(data * 2);
            }
        }, Math.random() * 1000);
    });
}

.then() は第一引数に成功時のコールバック関数、第二引数に失敗時のコールバック関数を指定します。エラーを考慮した呼び出し元は下記の様になります。

JavaScript
function sample_reject() {
    aFunc3(100).then(
        (data) => { console.log(data); },    // 成功時の処理
        (e) => { console.log(e); }           // 失敗時の処理
    );
}

上記は、下記の様に記述することもできます。.catch(ng_callback) は、.then(undefined, ng_callback) と同じ意味を持ちます。Promise は一度エラーが発生すると、最初に ng_callback 関数が指定されるまで、then 処理をスキップします。

JavaScript
function sample_catch() {
    aFunc3(100).then((data) => {
        console.log(data);
        return aFunc3(data);
    })
    .then((data) => {
        console.log(data);
        return aFunc3(data);
    })
    .then((data) => {
        console.log(data);
    })
    .catch((e) => {
        console.log(e);
    });
}

throwを伴うエラー処理

.catch() はまた、処理中に発生した throw をキャッチすることもできます。下記の例では、aFunc3() 内部で発生したエラーや、2番目の処理で発生した例外を .catch() が受け止めます。

JavaScript
function sample_catch_with_throw() {
    aFunc3(100).then((data) => {
        console.log(data);
        return aFunc3(data);
    })
    .then((data) => {
        console.log(data);
        throw new Error('ERROR!!!');
    })
    .then((data) => {
        console.log(data);
    })
    .catch((e) => {
        console.log(e);
    });
}

Finally

.catch() の後ろに .then() を加えることで、成功時にも、失敗時にも常に実行される Finally のような処理を追加することができます。

JavaScript
function sample_finally() {
    aFunc3(100).then((data) => {
        console.log(data);
        return aFunc3(data);
    })
    .then((data) => {
        console.log(data);
        return aFunc3(data);
    })
    .then((data) => {
        console.log(data);
    })
    .catch((e) => {
        console.log(e);
    })
    .then(() => {
        console.log('*** Finally ***');
    });
}

ES2018(ES9) では、.finally() がサポートされました。

function sample_finally2() {
    aFunc3(100).then((data) => {
        console.log(data);
        return aFunc3(data);
    })
    .then((data) => {
        console.log(data);
        return aFunc3(data);
    })
    .then((data) => {
        console.log(data);
    })
    .catch((e) => {
        console.log("catch");
        console.log(e);
    })
    .finally(() => {
        console.log('*** Finally ***');
    });
}

すべてのタスクが完了したら(Promise.all())

Promise.all() は配列で指定されたすべての Promise タスクを待ち合わせ、すべてのタスクが完了した時点で .then() のコールバック関数を呼び出します。

JavaScript
function taskA() {
    return new Promise((callback) => {
        console.log("taskA start.");
        setTimeout(function() {
            console.log("taskA end.");
            callback();
        }, Math.random() * 3000);
    });
}
function taskB() {
    return new Promise((callback) => {
        console.log("taskB start.");
        setTimeout(function() {
            console.log("taskB end.");
            callback();
        }, Math.random() * 3000);
    });
}
function sample_all() {
    p1 = taskA();
    p2 = taskB();
    Promise.all([p1, p2]).then(() => {
        console.log("taskA and taskB are finished.");
    });
}

いずれかのタスクが完了したら(Promise.race())

Promise.race() は配列で指定された Promise タスクを待ち合わせ、いずれかひとつのタスクが完了した時点で、.then() のコールバック関数を呼び出します。

JavaScript
function sample_race() {
    p1 = taskA();
    p2 = taskB();
    Promise.race([p1, p2]).then(() => {
        console.log("taskA or task B is finished.");
    });
}

いずれかのタスクが完了したら(Promise.any())

ES2021 では Promise.any() が追加されました。Promise.race() と同様、いずれかのタスクの完了を待ちますが、race() がいずれかのタスクが成功(resolve)または失敗(reject)した時点で終了するのに対し、any() はいずれかのタスクが成功(resolve)した時のみ終了します。

JavaScript
function sample_race() {
    p1 = taskA();
    p2 = taskB();
    Promise.any([p1, p2]).then(() => {
        console.log("taskA or task B is finished.");
    });
}

すべてのタスクが成功・失敗に関わらず完了したら(Promise.allSettled())

Promise.all() では、指定したタスクのいずれか一つがエラーになるとそこで待ち合わせを完了してしまいますが、ES2020 でサポートされた Promise.allSettled() を用いると、タスクがエラーとなっても、すべてのタスクが成功終了するか、エラー終了するまで、処理を待ち合わせることが可能となります。

JavaScript
p1 = Promise.resolve("OK1");
p2 = Promise.reject("NG2");
p3 = Promise.resolve("OK3");
Promise.allSettled([p1, p2, p3]).then(
   resolveList => resolveList.forEach(res => console.log(res)),
   rejectList  => rejectList.forEach(rej => console.log(rej))
);
// => {status: "fulfilled", value: "OK1"}
// => {status: "rejected", reason: "NG2"}
// => {status: "fulfilled", value: "OK3"}

非同期関数を同期関数っぽく呼び出す(async/await)

ES2017 では、Promise に加え、async/await がサポートされました。こちらも、Internet Explorer を除く大半のモダンブラウザで利用可能です。async と await を用いることで、Promise に対応した非同期関数を、同期関数の様に呼び出すことが可能となります。同期関数の様に呼び出したい非同期関数を呼び出す際に await をつけます。await を呼び出す関数に async をつけます。

JavaScript
async function sample_async_await() {
    var val = 100;
    val = await aFunc2(val);
    console.log(val);                    // 200
    val = await aFunc2(val);
    console.log(val);                    // 400
    val = await aFunc2(val);
    console.log(val);                    // 800
}

エラー処理に対応するコードは下記の様になります。

JavaScript
async function sample_async_await_with_catch() {
    var val = 100;
    try {
        val = await aFunc3(val);
        console.log(val);
        val = await aFunc3(val);
        console.log(val);
        val = await aFunc3(val);
        console.log(val);
    } catch (e) {
        console.log(e);
    }
}

await を呼び出す関数には async をつける必要がありますが、ES2022 からトップレベルからの呼び出しのみは async 関数でラップしなくても await を呼び出せるようになりました。詳細は「トップレベル await」を参照してください。

for await ... of ...

ES2018(ES9) では、非同期なイテラブルオブジェクトに対して、for await (... of ...) でループを回せるようになりました。例えば、サーバからデータを1件ずつ読み込む非同期処理を、同期処理の様に for ループで記述することが可能となります。

JavaScript
var asyncIterableObject = {
  [Symbol.asyncIterator]() {
    return {
      count: 0,
      next() {
        return new Promise((callback) => {
          setTimeout(() => {
            callback({
              value: this.count,
              done: (this.count < 5) ? false : true,
            })
            this.count++;
          }, Math.random() * 1000);
        });
      }
    };
  }
};

async function for_await_of() {
   for await (num of asyncIterableObject) {
     console.log(num);                      // 0, 1, 2, 3, 4
   }
};

Copyright (C) 2019-2022 杜甫々
初版:2019年2月11日 最終更新:2022年7月3日
http://www.tohoho-web.com/ex/promise.html