Javascriptの非同期処理を本質から理解する エラーファーストコールバック・Promise

非同期処理

参考にさせていただいたサイト↓

jsprimer.net

非同期処理とは

非同期処理はコードがz軸に沿って浮いた状態のイメージとなる。例えば

console.log("1. setTimeoutに登録した関数を1秒後に実行します");

setTimeout(() => {
  console.log("3. 非同期処理が実行されています");
}, 1000);

console.log("2. 非同期処理より先に実行されます");

この場合、setTimeoutに登録した関数は非同期処理となり、プログラムは上から順に実行されるという僕の思い込みは打ち砕かれます。僕のイメージでは、setTimeoutに登録(引数として渡した)アロー関数は、1000msの間浮いた状態となります。

非同期処理と例外処理

非同期処理をtry...catchで例外キャッチしようとしてもできません。

try {
    setTimeout(() => {
        throw new Error("非同期的なエラー");
    }, 10);
} catch (error) {
    // 非同期エラーはキャッチできないため、この行は実行されません
}

try...catchの中に非同期処理を記述してはいますが、try...catchが実行される時、非同期処理は浮いているため、例外をキャッチできません。次のようにすれば例外をキャッチできます

setTimeout(() => {
  try {
    throw new Error("エラー");
  } catch (error) {
    console.log("エラーをキャッチできる");
  }
}

ですが、これでは非同期処理の外側に例外が発生したことを伝えることができません。なぜ外側に例外処理を伝える必要があるのかを考えていきます

例えば、サーバからデータを取ってくるという非同期な処理と、データを加工するという処理をするとします。もしこれを非同期処理の中で全部実行するなら例外を外側へ伝える必要はありあませんが、データを加工する処理を分離すると、そのデータを加工する処理に、「データ取ってくるの失敗しちゃった😭」と伝える必要があるのです。

そしたらデータを加工する処理は条件分岐で、成功したらconst title = response.title、失敗したらリダイレクトとすることがきます。

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

具体例をみてみます

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 {
        // この行は実行されません
    }
});

分離しなきゃいいじゃん!って思うかもしれませんが、データ加工処理はページ毎などに変わる可能性が高いので、その度にdummyFetch関数を記述しなければいけなくなり、やばいほどに冗長です。

ちなみに、上のコードように、データ加工処理をするコールバック関数の第一引数にエラーオブジェクトを入れて、その中で条件分岐する方法をエラーファーストコールバックと呼ぶそうです

今このような記述をすることはほとんどないと思いますが、過去のコードでガチャガチャするときに必要になると思います。

Promise

エラーファーストコールバックとは異なり、非同期処理(asyncPromiseTask関数)はPromiseインスタンスを返しています。 その返されたPromiseインスタンスに対して、成功と失敗時の処理をそれぞれコールバック関数として渡すという形になります。

// asyncPromiseTask関数はPromiseインスタンスを返す
asyncPromiseTask().then(()=> {
    // 非同期処理が成功したときの処理
}).catch(() => {
    // 非同期処理が失敗したときの処理
});

Promiseインスタンスの生成

イメージとしては、非同期処理を行なう関数をpromiseインスタンスに格納する。

const executor = (resolve, reject) => {
    // 非同期の処理が成功したときはresolveを呼ぶ
    // 非同期の処理が失敗したときはrejectを呼ぶ
};
const promise = new Promise(executor);

条件分岐

thenメソッドで、成功時と失敗時の関数を登録します。

// `Promise`インスタンスを作成
const promise = new Promise((resolve, reject) => {
  if(true) {
    resolve();
  } else {
    reject();
  }
});
const onFulfilled = () => {
    console.log("resolveされたときに呼ばれる");
};
const onRejected = () => {
    console.log("rejectされたときに呼ばれる");
};
// `then`メソッドで成功時と失敗時に呼ばれるコールバック関数を登録
promise.then(onFulfilled, onRejected);

ただ、これだとpromiseインスタンスが一つしかないため、promiseインスタンス工場を作成して、何度呼び出しても、新鮮なPromiseインスタンスを使用することができます。そしてこの例は上の例よりもよく使う形に近い表現で書かれています

function dummyFetch(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path.startsWith("/success")) {
                resolve({ body: `Response body of ${path}` });
            } else {
                reject(new Error("NOT FOUND"));
            }
        }, 1000 * Math.random());
    });
}
// `then`メソッドで成功時と失敗時に呼ばれるコールバック関数を登録
// /success/data のリソースは存在するので成功しonFulfilledが呼ばれる
dummyFetch("/success/data").then(function onFulfilled(response) {
    console.log(response); // => { body: "Response body of /success/data" }
}, function onRejected(error) {
    // この行は実行されません
});

補足ですが、成功時だけ登録したい場合、第二引数を省略すれば良いですが、失敗時だけ登録したい場合は、第一引数にundefinedを書く必要があり、冗長で不恰好なため次のようにcatchで登録することができます

dummyFetch("/failure/data").catch(error => {
    console.log(error.message);
});

ここまで理解できれば多分あとはスラスラいけるのでここからはjs-primerさんに任せます。

続き↓

jsprimer.net

このあと読むと良い記事(ここでもjs-primer)

jsprimer.net