6時だョ!!全員集合!!

Rails・JavaScrictを中心にアウトプットします。

2022年2月4日 JavaScript (JS Primer) エラーファーストコールバック

ECMAScript 2015(ES2015)でPromiseが仕様に入るまで、非同期処理中に発生した例外を扱う仕様はありませんでした。

それより以前では"エラーファーストコールバック"というルールが使われていました。

エラーファーストコールバックとは

次のような非同期処理におけるコールバック関数の呼び出し方を決めたルールです。

  • 処理が失敗した場合は、コールバック関数の1番目の引数にエラーオブジェクトを渡して呼び出す
  • 処理が成功した場合は、コールバック関数の1番目の引数にはnullを渡し、2番目以降の引数に成功時の結果を渡して呼び出す

つまり、"ひとつのコールバック関数で失敗した場合と成功した場合の両方を扱うルール"になります。

コード例

以下のdummyFetch関数は第一引数にパスを受け取り、第2引数にエラーファーストコールバックスタイルの関数を受け取ります。

非同期処理の中で、実引数に設定したコールバック関数を呼び出します。

trueになった場合は実引数に渡す引数は第一引数がnull、第二引数がメッセージの含まれるオブジェクトとなり実行されます。

falseになった場合は実引数に渡す引数はエラーオブジェクトとなります。

function dummyFetch(path, callback) {
    setTimeout(() => {
        // /success からはじまるパスにはリソースがあるという設定
        if (path.startsWith("/success")) {
            callback(null, { body: `Response body of ${path}` });
        } else {
            callback(new Error("NOT FOUND"));
        }
    }, 1000 * Math.random());
}
// /success/data にリソースが存在するので、`response`にはデータが入る
dummyFetch("/success/data", (error, response) => {
    if (error) {
        // この行は実行されません
    } else {
        console.log(response); // => { body: "Response body of /success/data" }
    }
});
// /failure/data にリソースは存在しないので、`error`にはエラーオブジェクトが入る
dummyFetch("/failure/data", (error, response) => {
    if (error) {
        console.log(error.message); // => "NOT FOUND"
    } else {
        // この行は実行されません
    }
});

1000 * Math.random();とあります。

Math.random;は0以上、1未満の値を返します。つまりここでは0〜1000未満の値をランダムで返します。ここで重要なのが整数ではない点です。 実行結果を見てみると。

101.36167119070727
944.2996678611651

このような結果が返ってきます。

このMath.random();の結果を整数で受け取りたい場合(配列のインデックス番号をランダムで受け取りたい等)のときにはMath.floor()を使いこのように書きます。 console.log(Math.floor(1000 * Math.random())); 結果はこのように返ってきます。

853
529

0〜1000未満の値を整数で取得できました。

今回のコードを実行してみると

NOT FOUND
{ body: 'Response body of /success/data' }
NOT FOUND
{ body: 'Response body of /success/data' }
NOT FOUND
NOT FOUND
NOT FOUND
{ body: 'Response body of /success/data' }

このように実行結果が前後します。コードは上から順番に読み込まれていきますが、setTimeoutの第二引数の待ち時間の間にも後の関数も実行されていきます。 例えますと、function宣言がお店、実行関数がお客様だとします。 1人目のお客様の注文が入りましたが、商品が手元に来るまでの待ち時間がランダムです。その待ち時間の間に2人目のお客様の注文も入ります。 1人目の待ち時間が30秒でしたが、2人目の待ち時間は20秒だった場合に2人目のお客様の方が先に商品を受け取る結果になります。 これがこのコード内でも行われており、実行結果がエラーにならない場合(1人目)とエラーになる場合(2人目)の実行結果が前後するわけです。

以下、本書の内容に戻ります。

このように、コールバック関数の1番目の引数にはエラーオブジェクトまたはnullを入れ、それ以降の引数にデータを渡すというルールをエラーファーストコールバックと呼びます。

非同期処理中に例外が発生して生じたエラーをコールバック関数で受け取る方法はほかにもあります。 たとえば、成功したときに呼び出すコールバック関数と失敗したときに呼び出すコールバック関数の2つを受け取る方法があります。 先ほどのdummyFetch関数を2種類のコールバック関数を受け取る形に変更すると次のような実装になります。

/**
 * リソースの取得に成功した場合は`successCallback(レスポンス)`を呼び出す
 * リソースの取得に失敗した場合は`failureCallback(エラー)`を呼び出す
 */
function dummyFetch(path, successCallback, failureCallback) {
    setTimeout(() => {
        if (path.startsWith("/success")) {
            successCallback({ body: `Response body of ${path}` });
        } else {
            failureCallback(new Error("NOT FOUND"));
        }
    }, 1000 * Math.random());
}

このように非同期処理の中で例外が発生した場合に、その例外を非同期処理の外へ伝える方法にはさまざまな手段が考えられます。 エラーファーストコールバックはその形を決めた共通のルールの1つです。 ルールを決めることのメリットとして、エラーハンドリングのパターン化ができます。

しかし、エラーファーストコールバックは非同期処理におけるエラーハンドリングの書き方を決めたただのルールであって仕様ではありません。 そのため、エラーファーストコールバックというルールを破っても、問題があるわけではありません。

しかしながら、最初に書いたようにJavaScriptでは非同期処理を扱うケースが多いため、ただのルールではなくECMAScriptの仕様として非同期処理を扱う方法が求められていました。 そこで、ES2015ではPromiseという非同期処理を扱うビルトインオブジェクトが導入されました。

これから、ES2015で導入されたPromiseについて学びます。

メモ

  • エラーファーストコールバックとは、コールバック関数の1番目の引数にはエラーオブジェクトまたはnullを入れ、それ以降の引数にデータを渡すというルールを指します。
  • ルールであり、仕様ではありませんが、パターン化が出来る点がメリットとされていました。

参考

JS primer エラーファーストコールバック

String.prototype.startsWith()

Math.random()

Math.floor()